checkpoint
parent
f4144015dd
commit
f74bd81dcb
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
factory dao.Factory
|
||||
mx MetricsServer
|
||||
}
|
||||
)
|
||||
|
||||
// NewCluster returns a new cluster info resource.
|
||||
func NewCluster(c client.Connection, mx MetricsServer) *Cluster {
|
||||
return NewClusterWithArgs(c, mx)
|
||||
func NewCluster(f dao.Factory) *Cluster {
|
||||
return &Cluster{
|
||||
factory: f,
|
||||
mx: client.NewMetricsServer(f.Client()),
|
||||
}
|
||||
|
||||
// NewClusterWithArgs for tests only!
|
||||
func NewClusterWithArgs(c client.Connection, mx MetricsServer) *Cluster {
|
||||
return &Cluster{client: c, mx: mx}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,9 +21,9 @@ import (
|
|||
var ExitStatus = ""
|
||||
|
||||
const (
|
||||
splashTime = 1
|
||||
splashDelay = 1 * time.Second
|
||||
clusterRefresh = 5 * time.Second
|
||||
statusIndicatorFmt = "[orange::b]K9s [aqua::]%s [white::]%s:%s:%s [lawngreen::]%s%%[white::]::[darkturquoise::]%s%%"
|
||||
maxConRetry = 5
|
||||
clusterInfoWidth = 50
|
||||
clusterInfoPad = 15
|
||||
)
|
||||
|
|
@ -39,6 +38,8 @@ type App struct {
|
|||
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
|
||||
go func() {
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,30 +360,14 @@ 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),
|
||||
}
|
||||
|
||||
if b.app.ConOK() {
|
||||
b.namespaceActions(aa)
|
||||
|
||||
if client.Can(b.meta.Verbs, "edit") {
|
||||
|
|
@ -386,6 +376,7 @@ func (b *Browser) refreshActions() {
|
|||
if client.Can(b.meta.Verbs, "delete") {
|
||||
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true)
|
||||
}
|
||||
}
|
||||
|
||||
if !dao.IsK9sMeta(b.meta) {
|
||||
aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) 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) 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) 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
|
||||
)
|
||||
|
||||
c.GetCell(row, 1).SetText(cluster.ContextName())
|
||||
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(cluster.ClusterName())
|
||||
c.GetCell(row, 1).SetText(data.Cluster)
|
||||
row++
|
||||
c.GetCell(row, 1).SetText(cluster.UserName())
|
||||
row += 2
|
||||
c.GetCell(row, 1).SetText(cluster.Version())
|
||||
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) + "%")
|
||||
|
||||
cell := c.GetCell(row, 1)
|
||||
cell.SetText(render.NAValue)
|
||||
cell = c.GetCell(row+1, 1)
|
||||
cell.SetText(render.NAValue)
|
||||
|
||||
if c.app.Conn().HasMetrics() {
|
||||
c.refreshMetrics(cluster, row)
|
||||
}
|
||||
c.updateStyle()
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,11 +139,13 @@ 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 nil
|
||||
}
|
||||
|
||||
func (c *Command) specialCmd(cmd string) bool {
|
||||
cmds := strings.Split(cmd, " ")
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue