Commit f74bd81d authored by derailed's avatar derailed
Browse files

checkpoint

parent f4144015
Showing with 418 additions and 269 deletions
+418 -269
......@@ -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)
---
......
<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
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)
}
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,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!"
a.BailOut()
}
// 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()
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))
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")
if a.conRetry > maxConRetry {
ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", a.conRetry)
a.BailOut()
}
if a.conRetry > 0 {
return
}
cpu := render.AsPerc(cmx.PercCPU)
if cpu == "0" {
cpu = render.NAValue
}
mem := render.AsPerc(cmx.PercMEM)
if mem == "0" {
mem = render.NAValue
}
// Reload alias
go func() {
if err := a.command.Reset(); err != nil {
log.Error().Err(err).Msgf("Command reset failed")
}
}()
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,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()
......
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()
}
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
c.updateStyle()
}
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
)
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++
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) 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.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,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)
}
......
......@@ -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
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment