From f74bd81dcb56396167c33214024aa6c94cb862a0 Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 24 Jan 2020 21:34:16 -0700 Subject: [PATCH] checkpoint --- README.md | 4 +- change_logs/release_v0.13.5.md | 24 +++++ internal/client/client.go | 1 + internal/client/config.go | 12 +++ internal/dao/describe.go | 1 - internal/dao/pod.go | 12 +-- internal/model/cluster.go | 45 +++++---- internal/model/cluster_info.go | 130 +++++++++++++++++++++++++ internal/model/table.go | 2 +- internal/render/row_event.go | 13 ++- internal/render/row_header.go | 4 +- internal/render/table_data.go | 4 +- internal/ui/deltas.go | 4 +- internal/ui/indicator.go | 43 +++++++++ internal/view/actions.go | 10 +- internal/view/app.go | 129 ++++++++++++------------- internal/view/browser.go | 59 +++++++----- internal/view/cluster_info.go | 171 ++++++++++----------------------- internal/view/command.go | 14 ++- internal/view/context.go | 3 + internal/view/port_forward.go | 2 +- 21 files changed, 418 insertions(+), 269 deletions(-) create mode 100644 change_logs/release_v0.13.5.md create mode 100644 internal/model/cluster_info.go diff --git a/README.md b/README.md index 47fa043a..dfec5388 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ for changes and offers subsequent commands to interact with observed Kubernetes ## Slack Channel Wanna discuss K9s features with your fellow `K9sers` or simply show your support for this tool? -Please Dial [K9s Slack](https://k9sers.slack.com/) + +* Channel: [K9ersSlack](https://k9sers.slack.com/) +* Invite: [K9slackers Invite](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) --- diff --git a/change_logs/release_v0.13.5.md b/change_logs/release_v0.13.5.md new file mode 100644 index 00000000..936c7edc --- /dev/null +++ b/change_logs/release_v0.13.5.md @@ -0,0 +1,24 @@ + + +# Release v0.13.5 + +## 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, please 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! + +--- +## Resolved Bugs/Features + +* [Issue #507](https://github.com/derailed/k9s/issues/507) + +--- + + © 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 9a757a5c..7669a86e 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -268,6 +268,7 @@ func (a *APIClient) reset() { a.mx.Lock() defer a.mx.Unlock() + a.cache = cache.NewLRUExpireCache(cacheSize) a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil } diff --git a/internal/client/config.go b/internal/client/config.go index 364e6236..2f2e406d 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "sync" + "time" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -13,6 +14,12 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" ) +const ( + defaultQPS = 100 + defaultBurst = 50 + defaultTimeout = 10 * time.Second +) + // Config tracks a kubernetes configuration. type Config struct { flags *genericclioptions.ConfigFlags @@ -280,6 +287,11 @@ func (c *Config) RESTConfig() (*restclient.Config, error) { if c.restConfig, err = c.flags.ToRESTConfig(); err != nil { return nil, err } + log.Debug().Msgf("REST_CONFIG %#v", c.restConfig) + c.restConfig.QPS = defaultQPS + c.restConfig.Burst = defaultBurst + c.restConfig.Timeout = defaultTimeout + log.Debug().Msgf("Connecting to API Server %s", c.restConfig.Host) return c.restConfig, nil diff --git a/internal/dao/describe.go b/internal/dao/describe.go index b7d9fbb7..9c549e80 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -9,7 +9,6 @@ import ( // Describe describes a resource. func Describe(c client.Connection, gvr client.GVR, path string) (string, error) { - log.Debug().Msgf("DESCRIBE %q::%q", gvr, path) mapper := RestMapper{Connection: c} m, err := mapper.ToRESTMapper() if err != nil { diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 8a382e3f..98ca8bfb 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -49,10 +49,8 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) { return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o) } - pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList) - if !ok { - log.Warn().Msgf("no metrics available for %q", p.gvr) - } + // No Deal! + pmx, _ := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList) return &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)}, nil } @@ -74,10 +72,8 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { return oo, err } - pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList) - if !ok { - log.Warn().Msgf("no metrics available for %q", p.gvr) - } + // No Deal! + pmx, _ := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList) var res []runtime.Object for _, o := range oo { diff --git a/internal/model/cluster.go b/internal/model/cluster.go index 625b45ef..54593dac 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -2,6 +2,7 @@ package model import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" v1 "k8s.io/api/core/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -25,26 +26,24 @@ type ( // Cluster represents a kubernetes resource. Cluster struct { - client client.Connection - mx MetricsServer + factory dao.Factory + mx MetricsServer } ) // NewCluster returns a new cluster info resource. -func NewCluster(c client.Connection, mx MetricsServer) *Cluster { - return NewClusterWithArgs(c, mx) -} - -// NewClusterWithArgs for tests only! -func NewClusterWithArgs(c client.Connection, mx MetricsServer) *Cluster { - return &Cluster{client: c, mx: mx} +func NewCluster(f dao.Factory) *Cluster { + return &Cluster{ + factory: f, + mx: client.NewMetricsServer(f.Client()), + } } // Version returns the current K8s cluster version. func (c *Cluster) Version() string { - info, err := c.client.ServerVersion() + info, err := c.factory.Client().ServerVersion() if err != nil { - return "n/a" + return NA } return info.GitVersion @@ -52,32 +51,42 @@ func (c *Cluster) Version() string { // ContextName returns the context name. func (c *Cluster) ContextName() string { - n, err := c.client.Config().CurrentContextName() + n, err := c.factory.Client().Config().CurrentContextName() if err != nil { - return "n/a" + return NA } return n } // ClusterName returns the cluster name. func (c *Cluster) ClusterName() string { - n, err := c.client.Config().CurrentClusterName() + n, err := c.factory.Client().Config().CurrentClusterName() if err != nil { - return "n/a" + return NA } return n } // UserName returns the user name. func (c *Cluster) UserName() string { - n, err := c.client.Config().CurrentUserName() + n, err := c.factory.Client().Config().CurrentUserName() if err != nil { - return "n/a" + return NA } return n } // Metrics gathers node level metrics and compute utilization percentages. -func (c *Cluster) Metrics(nn *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *client.ClusterMetrics) error { +func (c *Cluster) Metrics(mx *client.ClusterMetrics) error { + nn, err := dao.FetchNodes(c.factory, "") + if err != nil { + return err + } + + nmx, err := c.mx.FetchNodesMetrics() + if err != nil { + return err + } + return c.mx.ClusterLoad(nn, nmx, mx) } diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go new file mode 100644 index 00000000..cfc14e10 --- /dev/null +++ b/internal/model/cluster_info.go @@ -0,0 +1,130 @@ +package model + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" +) + +type ClusterInfoListener interface { + ClusterInfoChanged(prev, curr ClusterMeta) + ClusterInfoUpdated(ClusterMeta) +} + +const NA = "n/a" + +// ClusterMeta represents cluster meta data. +type ClusterMeta struct { + Context, Cluster string + User string + K9sVer, K8sVer string + Cpu, Mem float64 +} + +// NewClusterMeta returns a new instance. +func NewClusterMeta() ClusterMeta { + return ClusterMeta{ + Context: NA, + Cluster: NA, + User: NA, + K9sVer: NA, + K8sVer: NA, + Cpu: 0, + Mem: 0, + } +} + +// Deltas diffs cluster meta return true if different, false otherwise. +func (c ClusterMeta) Deltas(n ClusterMeta) bool { + if render.AsPerc(c.Cpu) != render.AsPerc(n.Cpu) { + return true + } + if render.AsPerc(c.Mem) != render.AsPerc(n.Mem) { + return true + } + + return c.Context != n.Context || + c.Cluster != n.Cluster || + c.User != n.User || + c.K8sVer != n.K8sVer || + c.K9sVer != n.K9sVer +} + +// ClusterInfo models cluster metadata. +type ClusterInfo struct { + cluster *Cluster + data ClusterMeta + version string + listeners []ClusterInfoListener +} + +// NewClusterInfo returns a new instance. +func NewClusterInfo(f dao.Factory, version string) *ClusterInfo { + return &ClusterInfo{ + cluster: NewCluster(f), + data: NewClusterMeta(), + version: version, + } +} + +// Reset resets context and reload. +func (c *ClusterInfo) Reset(f dao.Factory) { + c.cluster, c.data = NewCluster(f), NewClusterMeta() + c.Refresh() +} + +// Refresh fetches latest cluster meta. +func (c *ClusterInfo) Refresh() { + log.Debug().Msgf("Refreshing ClusterInfo...") + data := NewClusterMeta() + data.Context = c.cluster.ContextName() + data.Cluster = c.cluster.ClusterName() + data.User = c.cluster.UserName() + data.K9sVer = c.version + data.K8sVer = c.cluster.Version() + + var mx client.ClusterMetrics + if err := c.cluster.Metrics(&mx); err == nil { + data.Cpu, data.Mem = mx.PercCPU, mx.PercMEM + } + + if c.data.Deltas(data) { + c.fireMetaChanged(c.data, data) + } else { + c.fireNoMetaChanged(data) + } + c.data = data +} + +// AddListener adds a new model listener. +func (c *ClusterInfo) AddListener(l ClusterInfoListener) { + c.listeners = append(c.listeners, l) +} + +// RemoveListener delete a listener from the list. +func (c *ClusterInfo) RemoveListener(l ClusterInfoListener) { + victim := -1 + for i, lis := range c.listeners { + if lis == l { + victim = i + break + } + } + + if victim >= 0 { + c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...) + } +} + +func (c *ClusterInfo) fireMetaChanged(prev, cur ClusterMeta) { + for _, l := range c.listeners { + l.ClusterInfoChanged(prev, cur) + } +} + +func (c *ClusterInfo) fireNoMetaChanged(data ClusterMeta) { + for _, l := range c.listeners { + l.ClusterInfoUpdated(data) + } +} diff --git a/internal/model/table.go b/internal/model/table.go index 8d320c26..80595f4a 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -230,7 +230,7 @@ func (t *Table) reconcile(ctx context.Context) error { oo, err = []runtime.Object{o}, e } if err != nil { - return err + log.Error().Err(err).Msg("Reconcile failed to list resource") } var rows render.Rows diff --git a/internal/render/row_event.go b/internal/render/row_event.go index 3b962fcd..aeb3d0e5 100644 --- a/internal/render/row_event.go +++ b/internal/render/row_event.go @@ -61,17 +61,16 @@ func (r RowEvent) Clone() RowEvent { } } -// Changed returns true if the row changed. -func (r RowEvent) Changed(re RowEvent) bool { +// Diff returns true if the row changed. +func (r RowEvent) Diff(re RowEvent) bool { if r.Kind != re.Kind { - log.Debug().Msgf("KIND Changed") return true } if !reflect.DeepEqual(r.Deltas, re.Deltas) { - log.Debug().Msgf("DELTAS CHANGED") return true } + // BOZO!! Canned?? Skip age colum return !reflect.DeepEqual(r.Row.Fields[:len(r.Row.Fields)-1], re.Row.Fields[:len(re.Row.Fields)-1]) } @@ -80,14 +79,14 @@ func (r RowEvent) Changed(re RowEvent) bool { // RowEvents a collection of row events. type RowEvents []RowEvent -// Changed returns true if the header changed. -func (rr RowEvents) Changed(r RowEvents) bool { +// Diff returns true if the event changed. +func (rr RowEvents) Diff(r RowEvents) bool { if len(rr) != len(r) { return true } for i := range rr { - if rr[i].Changed(r[i]) { + if rr[i].Diff(r[i]) { return true } } diff --git a/internal/render/row_header.go b/internal/render/row_header.go index 4fe1e20b..4b34ea1a 100644 --- a/internal/render/row_header.go +++ b/internal/render/row_header.go @@ -36,8 +36,8 @@ func (hh HeaderRow) Clear() HeaderRow { return HeaderRow{} } -// Changed returns true if the header changed. -func (hh HeaderRow) Changed(h HeaderRow) bool { +// Diff returns true if the header changed. +func (hh HeaderRow) Diff(h HeaderRow) bool { if len(hh) != len(h) { return true } diff --git a/internal/render/table_data.go b/internal/render/table_data.go index 07175be4..9cb7f293 100644 --- a/internal/render/table_data.go +++ b/internal/render/table_data.go @@ -88,10 +88,10 @@ func (t *TableData) Diff(table TableData) bool { if t.Namespace != table.Namespace { return true } - if t.Header.Changed(table.Header) { + if t.Header.Diff(table.Header) { return true } - if t.RowEvents.Changed(table.RowEvents) { + if t.RowEvents.Diff(table.RowEvents) { return true } diff --git a/internal/ui/deltas.go b/internal/ui/deltas.go index 049a89ea..99b8c73f 100644 --- a/internal/ui/deltas.go +++ b/internal/ui/deltas.go @@ -14,9 +14,9 @@ const ( // DeltaSign signals a diff. DeltaSign = "Δ" // PlusSign signals inc. - PlusSign = "↑" + PlusSign = "[red::b]↑" // MinusSign signal dec. - MinusSign = "↓" + MinusSign = "[green::b]↓" ) var percent = regexp.MustCompile(`\A(\d+)\%\z`) diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index a1f7fa5b..d54bcb88 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -6,6 +6,8 @@ import ( "time" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/gdamore/tcell" ) @@ -43,6 +45,36 @@ func (s *StatusIndicator) StylesChanged(styles *config.Styles) { s.SetTextColor(styles.FgColor()) } +const statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" + +func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) { + s.app.QueueUpdateDraw(func() { + s.SetPermanent(fmt.Sprintf( + statusIndicatorFmt, + data.K9sVer, + data.Cluster, + data.User, + data.K8sVer, + render.AsPerc(data.Cpu)+"%", + render.AsPerc(data.Mem)+"%", + )) + }) +} + +func (s *StatusIndicator) ClusterInfoChanged(prev, cur model.ClusterMeta) { + s.app.QueueUpdateDraw(func() { + s.SetPermanent(fmt.Sprintf( + statusIndicatorFmt, + cur.K9sVer, + cur.Cluster, + cur.User, + cur.K8sVer, + AsPercDelta(prev.Cpu, cur.Cpu), + AsPercDelta(prev.Cpu, cur.Mem), + )) + }) +} + // SetPermanent sets permanent title to be reset to after updates func (s *StatusIndicator) SetPermanent(info string) { s.permanent = info @@ -93,3 +125,14 @@ func (s *StatusIndicator) setText(msg string) { } }(ctx) } + +// Helpers... + +func AsPercDelta(ov, nv float64) string { + prev, cur := render.AsPerc(ov), render.AsPerc(nv) + if cur == "0" { + return render.NAValue + } + + return cur + "%" + Deltas(prev, cur) +} diff --git a/internal/view/actions.go b/internal/view/actions.go index c371f79f..c15ed77c 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -52,19 +52,18 @@ func inScope(scopes, aliases []string) bool { func hotKeyActions(r Runner, aa ui.KeyActions) { hh := config.NewHotKeys() if err := hh.Load(); err != nil { - log.Error().Err(err).Msgf("Loading HOTKEYS") return } for k, hk := range hh.HotKey { key, err := asKey(hk.ShortCut) if err != nil { - log.Error().Err(err).Msg("HOT-KEY Unable to map hotkey shortcut to a key") + log.Warn().Err(err).Msg("HOT-KEY Unable to map hotkey shortcut to a key") continue } _, ok := aa[key] if ok { - log.Error().Err(fmt.Errorf("HOT-KEY Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") + log.Warn().Err(fmt.Errorf("HOT-KEY Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") continue } aa[key] = ui.NewSharedKeyAction( @@ -86,7 +85,6 @@ func gotoCmd(r Runner, cmd string) ui.ActionHandler { func pluginActions(r Runner, aa ui.KeyActions) { pp := config.NewPlugins() if err := pp.Load(); err != nil { - log.Warn().Msgf("No plugin configuration found") return } @@ -96,12 +94,12 @@ func pluginActions(r Runner, aa ui.KeyActions) { } key, err := asKey(plugin.ShortCut) if err != nil { - log.Error().Err(err).Msg("Unable to map plugin shortcut to a key") + log.Warn().Err(err).Msg("Unable to map plugin shortcut to a key") continue } _, ok := aa[key] if ok { - log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") + log.Warn().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") continue } aa[key] = ui.NewKeyAction( diff --git a/internal/view/app.go b/internal/view/app.go index 5700aa5a..aeee999b 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -10,7 +10,6 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/watch" "github.com/derailed/tview" @@ -22,23 +21,25 @@ import ( var ExitStatus = "" const ( - splashTime = 1 - clusterRefresh = 5 * time.Second - statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%" - clusterInfoWidth = 50 - clusterInfoPad = 15 + splashDelay = 1 * time.Second + clusterRefresh = 5 * time.Second + maxConRetry = 5 + clusterInfoWidth = 50 + clusterInfoPad = 15 ) // App represents an application view. type App struct { *ui.App - Content *PageStack - command *Command - factory *watch.Factory - version string - showHeader bool - cancelFn context.CancelFunc + Content *PageStack + command *Command + factory *watch.Factory + version string + showHeader bool + cancelFn context.CancelFunc + conRetry int + clusterModel *model.ClusterInfo } // NewApp returns a K9s app instance. @@ -51,11 +52,16 @@ func NewApp(cfg *config.Config) *App { a.InitBench(cfg.K9s.CurrentCluster) a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles) - a.Views()["clusterInfo"] = NewClusterInfo(&a, client.NewMetricsServer(cfg.GetConnection())) + a.Views()["clusterInfo"] = NewClusterInfo(&a) return &a } +// ConOK checks the connection is cool, returns false otherwise. +func (a *App) ConOK() bool { + return a.conRetry == 0 +} + // Init initializes the application. func (a *App) Init(version string, rate int) error { a.version = version @@ -80,15 +86,17 @@ func (a *App) Init(version string, rate int) error { a.factory = watch.NewFactory(a.Conn()) a.initFactory(ns) + a.clusterModel = model.NewClusterInfo(a.factory, version) + a.clusterModel.AddListener(a.clusterInfo()) + a.clusterModel.AddListener(a.statusIndicator()) + a.clusterModel.Refresh() + a.command = NewCommand(a) if err := a.command.Init(); err != nil { return err } - a.clusterInfo().Init(version) - if a.Config.K9s.GetHeadless() { - a.refreshIndicator() - } + a.clusterInfo().Init() main := tview.NewFlex().SetDirection(tview.FlexRow) main.AddItem(a.statusIndicator(), 1, 1, false) @@ -129,7 +137,6 @@ func (a *App) toggleHeader(flag bool) { } else { flex.RemoveItemAtIndex(0) flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false) - a.refreshIndicator() } } @@ -181,63 +188,49 @@ func (a *App) clusterUpdater(ctx context.Context) { log.Debug().Msg("ClusterInfo updater canceled!") return case <-time.After(clusterRefresh): - a.refreshClusterInfo() + a.refreshCluster() } } } -// BOZO!! Refact to use model/view strategy. -func (a *App) refreshClusterInfo() { - if !a.Conn().CheckConnectivity() { - ExitStatus = "Lost K8s connection. Bailing out!" +func (a *App) refreshCluster() { + c := a.Content.Top() + + // Check conns + if ok := a.Conn().CheckConnectivity(); ok { + if a.conRetry > 0 { + if c != nil { + c.Start() + } + a.Status(ui.FlashInfo, "K8s connectivity OK") + } + a.conRetry = 0 + } else { + a.conRetry++ + log.Warn().Msgf("Conn check failed (%d)", a.conRetry) + if c != nil { + c.Stop() + } + a.Status(ui.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", a.conRetry)) + + } + if a.conRetry > maxConRetry { + ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", a.conRetry) a.BailOut() } + if a.conRetry > 0 { + return + } + // Reload alias - if err := a.command.Reset(); err != nil { - log.Error().Err(err).Msgf("Command reset failed") - } - a.QueueUpdateDraw(func() { - if !a.showHeader { - a.refreshIndicator() - } else { - a.clusterInfo().refresh() + go func() { + if err := a.command.Reset(); err != nil { + log.Error().Err(err).Msgf("Command reset failed") } - }) -} + }() -func (a *App) refreshIndicator() { - mx := client.NewMetricsServer(a.Conn()) - cluster := model.NewCluster(a.Conn(), mx) - var cmx client.ClusterMetrics - nos, nmx, err := fetchResources(a) - if err != nil { - log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator") - return - } - - if err := cluster.Metrics(nos, nmx, &cmx); err != nil { - log.Error().Err(err).Msgf("unable to refresh cluster statusIndicator") - return - } - - cpu := render.AsPerc(cmx.PercCPU) - if cpu == "0" { - cpu = render.NAValue - } - mem := render.AsPerc(cmx.PercMEM) - if mem == "0" { - mem = render.NAValue - } - - a.statusIndicator().SetPermanent(fmt.Sprintf( - statusIndicatorFmt, - a.version, - cluster.ClusterName(), - cluster.UserName(), - cluster.Version(), - cpu, - mem, - )) + // Update cluster info + a.clusterModel.Refresh() } func (a *App) switchNS(ns string) bool { @@ -276,7 +269,7 @@ func (a *App) switchCtx(name string, loadPods bool) error { if err := a.gotoResource("pods", true); loadPods && err != nil { a.Flash().Err(err) } - a.refreshClusterInfo() + a.clusterModel.Reset(a.factory) a.ReloadStyles(name) } @@ -299,7 +292,7 @@ func (a *App) Run() error { a.Resume() go func() { - <-time.After(splashTime * time.Second) + <-time.After(splashDelay) a.QueueUpdateDraw(func() { a.Main.SwitchToPage("main") }) diff --git a/internal/view/browser.go b/internal/view/browser.go index ca1e0d94..ac3e9f24 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -50,7 +50,7 @@ func (b *Browser) Init(ctx context.Context) error { return err } ns := client.CleanseNamespace(b.app.Config.ActiveNamespace()) - if dao.IsK8sMeta(b.meta) { + if dao.IsK8sMeta(b.meta) && b.app.ConOK() { if _, e := b.app.factory.CanForResource(ns, b.GVR(), client.MonitorAccess); e != nil { return e } @@ -92,6 +92,7 @@ func (b *Browser) SetInstance(path string) { func (b *Browser) Start() { b.Stop() + log.Debug().Msgf("BROWSER started!") b.Table.Start() ctx := b.defaultContext() ctx, b.cancelFn = context.WithCancel(ctx) @@ -109,6 +110,7 @@ func (b *Browser) Stop() { if b.cancelFn == nil { return } + log.Debug().Msgf("BROWSER Stopped!") b.Table.Stop() b.cancelFn() b.cancelFn = nil @@ -137,6 +139,10 @@ func (b *Browser) Aliases() []string { // TableDataChanged notifies view new data is available. func (b *Browser) TableDataChanged(data render.TableData) { + if !b.app.ConOK() { + return + } + b.app.QueueUpdateDraw(func() { b.refreshActions() b.Update(data) @@ -354,37 +360,22 @@ func (b *Browser) defaultContext() context.Context { return ctx } -func (b *Browser) namespaceActions(aa ui.KeyActions) { - if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" { - return - } - b.namespaces = make(map[int]string, config.MaxFavoritesNS) - aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) - b.namespaces[0] = client.NamespaceAll - index := 1 - for _, ns := range b.app.Config.FavNamespaces() { - if ns == client.NamespaceAll { - continue - } - aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) - b.namespaces[index] = ns - index++ - } -} - func (b *Browser) refreshActions() { aa := ui.KeyActions{ ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false), tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false), tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false), } - b.namespaceActions(aa) - if client.Can(b.meta.Verbs, "edit") { - aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) - } - if client.Can(b.meta.Verbs, "delete") { - aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) + if b.app.ConOK() { + b.namespaceActions(aa) + + if client.Can(b.meta.Verbs, "edit") { + aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) + } + if client.Can(b.meta.Verbs, "delete") { + aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) + } } if !dao.IsK9sMeta(b.meta) { @@ -402,6 +393,24 @@ func (b *Browser) refreshActions() { b.app.Menu().HydrateMenu(b.Hints()) } +func (b *Browser) namespaceActions(aa ui.KeyActions) { + if !b.meta.Namespaced || b.GetTable().Path != "" { + return + } + b.namespaces = make(map[int]string, config.MaxFavoritesNS) + aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) + b.namespaces[0] = client.NamespaceAll + index := 1 + for _, ns := range b.app.Config.FavNamespaces() { + if ns == client.NamespaceAll { + continue + } + aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) + b.namespaces[index] = ns + index++ + } +} + func (b *Browser) simpleDelete(selections []string, msg string) { dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() { b.ShowDeleted() diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index 06f9cba8..7999b4d1 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -1,92 +1,51 @@ package view import ( - "strings" - - "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) +var _ model.ClusterInfoListener = (*ClusterInfo)(nil) + // ClusterInfo represents a cluster info view. type ClusterInfo struct { *tview.Table app *App - mxs *client.MetricsServer styles *config.Styles } // NewClusterInfo returns a new cluster info view. -func NewClusterInfo(app *App, mx *client.MetricsServer) *ClusterInfo { +func NewClusterInfo(app *App) *ClusterInfo { return &ClusterInfo{ - app: app, Table: tview.NewTable(), - mxs: mx, + app: app, styles: app.Styles, } } // Init initializes the view. -func (c *ClusterInfo) Init(version string) { - cluster := model.NewCluster(c.app.Conn(), c.mxs) - +func (c *ClusterInfo) Init() { c.app.Styles.AddListener(c) - - row := c.initInfo(cluster) - row = c.initVersion(row, version, cluster) - - c.SetCell(row, 0, c.sectionCell("CPU")) - c.SetCell(row, 1, c.infoCell(render.NAValue)) - row++ - c.SetCell(row, 0, c.sectionCell("MEM")) - c.SetCell(row, 1, c.infoCell(render.NAValue)) - - c.refresh() + c.layout() } // StylesChanged notifies skin changed. func (c *ClusterInfo) StylesChanged(s *config.Styles) { c.styles = s c.SetBackgroundColor(s.BgColor()) - c.refresh() + c.updateStyle() } -func (c *ClusterInfo) initInfo(cluster *model.Cluster) int { - var row int - c.SetCell(row, 0, c.sectionCell("Context")) - c.SetCell(row, 1, c.infoCell(cluster.ContextName())) - row++ - - c.SetCell(row, 0, c.sectionCell("Cluster")) - c.SetCell(row, 1, c.infoCell(cluster.ClusterName())) - row++ - - c.SetCell(row, 0, c.sectionCell("User")) - c.SetCell(row, 1, c.infoCell(cluster.UserName())) - row++ - - return row -} - -func (c *ClusterInfo) initVersion(row int, version string, cluster *model.Cluster) int { - c.SetCell(row, 0, c.sectionCell("K9s Rev")) - c.SetCell(row, 1, c.infoCell(version)) - row++ - - c.SetCell(row, 0, c.sectionCell("K8s Rev")) - c.SetCell(row, 1, c.infoCell(cluster.Version())) - row++ - - return row +func (c *ClusterInfo) layout() { + for row, v := range []string{"Context", "Cluster", "User", "K9s Rev", "K8s Rev", "CPU", "MEM"} { + c.SetCell(row, 0, c.sectionCell(v)) + c.SetCell(row, 1, c.infoCell(render.NAValue)) + } } func (c *ClusterInfo) sectionCell(t string) *tview.TableCell { @@ -108,30 +67,46 @@ func (c *ClusterInfo) infoCell(t string) *tview.TableCell { return cell } -func (c *ClusterInfo) refresh() { - var ( - cluster = model.NewCluster(c.app.Conn(), c.mxs) - row int - ) +func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) { + c.app.QueueUpdateDraw(func() { + var row int + c.GetCell(row, 1).SetText(data.Context) + row++ + c.GetCell(row, 1).SetText(data.Cluster) + row++ + c.GetCell(row, 1).SetText(data.User) + row++ + c.GetCell(row, 1).SetText(data.K9sVer) + row++ + c.GetCell(row, 1).SetText(data.K8sVer) + row++ + c.GetCell(row, 1).SetText(render.AsPerc(data.Cpu) + "%") + row++ + c.GetCell(row, 1).SetText(render.AsPerc(data.Mem) + "%") - c.GetCell(row, 1).SetText(cluster.ContextName()) - row++ - c.GetCell(row, 1).SetText(cluster.ClusterName()) - row++ - c.GetCell(row, 1).SetText(cluster.UserName()) - row += 2 - c.GetCell(row, 1).SetText(cluster.Version()) - row++ + c.updateStyle() + }) +} - cell := c.GetCell(row, 1) - cell.SetText(render.NAValue) - cell = c.GetCell(row+1, 1) - cell.SetText(render.NAValue) +func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { + c.app.QueueUpdateDraw(func() { + var row int + c.GetCell(row, 1).SetText(curr.Context) + row++ + c.GetCell(row, 1).SetText(curr.Cluster) + row++ + c.GetCell(row, 1).SetText(curr.User) + row++ + c.GetCell(row, 1).SetText(curr.K9sVer) + row++ + c.GetCell(row, 1).SetText(curr.K8sVer) + row++ + c.GetCell(row, 1).SetText(ui.AsPercDelta(prev.Cpu, curr.Cpu)) + row++ + c.GetCell(row, 1).SetText(ui.AsPercDelta(prev.Mem, curr.Mem)) - if c.app.Conn().HasMetrics() { - c.refreshMetrics(cluster, row) - } - c.updateStyle() + c.updateStyle() + }) } func (c *ClusterInfo) updateStyle() { @@ -142,51 +117,3 @@ func (c *ClusterInfo) updateStyle() { c.GetCell(row, 1).SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))) } } - -func fetchResources(app *App) (*v1.NodeList, *mv1beta1.NodeMetricsList, error) { - nn, err := dao.FetchNodes(app.factory, "") - if err != nil { - return nil, nil, err - } - - mx := client.NewMetricsServer(app.factory.Client()) - nmx, err := mx.FetchNodesMetrics() - if err != nil { - return nil, nil, err - } - - return nn, nmx, nil -} - -func (c *ClusterInfo) refreshMetrics(cluster *model.Cluster, row int) { - nos, nmx, err := fetchResources(c.app) - if err != nil { - log.Warn().Err(err).Msgf("NodeMetrics failed") - return - } - - var cmx client.ClusterMetrics - if err := cluster.Metrics(nos, nmx, &cmx); err != nil { - log.Error().Err(err).Msgf("failed to retrieve cluster metrics") - } - cell := c.GetCell(row, 1) - cpu := render.AsPerc(cmx.PercCPU) - if cpu == "0" { - cpu = render.NAValue - } - cell.SetText(cpu + "%" + ui.Deltas(strip(cell.Text), cpu)) - row++ - - cell = c.GetCell(row, 1) - mem := render.AsPerc(cmx.PercMEM) - if mem == "0" { - mem = render.NAValue - } - cell.SetText(mem + "%" + ui.Deltas(strip(cell.Text), mem)) -} - -func strip(s string) string { - t := strings.Replace(s, ui.PlusSign, "", 1) - t = strings.Replace(t, ui.MinusSign, "", 1) - return t -} diff --git a/internal/view/command.go b/internal/view/command.go index 954d0b5e..41158e5b 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -5,6 +5,7 @@ import ( "fmt" "regexp" "strings" + "sync" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" @@ -23,6 +24,7 @@ type Command struct { app *App alias *dao.Alias + mx sync.Mutex } // NewCommand returns a new command. @@ -45,6 +47,9 @@ func (c *Command) Init() error { // Reset resets Command and reload aliases. func (c *Command) Reset() error { + c.mx.Lock() + defer c.mx.Unlock() + c.alias.Clear() if _, err := c.alias.Ensure(); err != nil { return err @@ -134,10 +139,12 @@ func (c *Command) run(cmd, path string, clearStack bool) error { } func (c *Command) defaultCmd() error { - if err := c.run(c.app.Config.ActiveView(), "", true); err != nil { + err := c.run(c.app.Config.ActiveView(), "", true) + if err != nil { log.Error().Err(err).Msgf("Saved command failed. Loading default view") + return c.run("pod", "", true) } - return c.run("pod", "", true) + return nil } func (c *Command) specialCmd(cmd string) bool { @@ -190,16 +197,13 @@ func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer { var view ResourceViewer if v.viewerFn != nil { - log.Debug().Msgf("Custom viewer for %s", gvr) view = v.viewerFn(client.NewGVR(gvr)) } else { - log.Debug().Msgf("Generic viewer for %s", gvr) view = NewBrowser(client.NewGVR(gvr)) } view.SetInstance(path) if v.enterFn != nil { - log.Debug().Msgf("SETTING CUSTOM ENTER ON %s", gvr) view.GetTable().SetEnterFn(v.enterFn) } diff --git a/internal/view/context.go b/internal/view/context.go index 9d8b5864..54ffb613 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -43,6 +43,9 @@ func (c *Context) useCtx(app *App, model ui.Tabular, gvr, path string) { } func useContext(app *App, name string) error { + if app.Content.Top() != nil { + app.Content.Top().Stop() + } res, err := dao.AccessorFor(app.factory, client.NewGVR("contexts")) if err != nil { return nil diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index 375c2fe3..551e2558 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -65,7 +65,7 @@ func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { if p.bench != nil { - p.App().Status(ui.FlashErr, "Benchmark Camceled!") + p.App().Status(ui.FlashErr, "Benchmark Canceled!") p.bench.Cancel() p.App().ClearStatus(true) return nil