checkpoint
parent
f4144015dd
commit
f74bd81dcb
|
|
@ -23,7 +23,9 @@ for changes and offers subsequent commands to interact with observed Kubernetes
|
||||||
## Slack Channel
|
## Slack Channel
|
||||||
|
|
||||||
Wanna discuss K9s features with your fellow `K9sers` or simply show your support for this tool?
|
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()
|
a.mx.Lock()
|
||||||
defer a.mx.Unlock()
|
defer a.mx.Unlock()
|
||||||
|
|
||||||
|
a.cache = cache.NewLRUExpireCache(cacheSize)
|
||||||
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -13,6 +14,12 @@ import (
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultQPS = 100
|
||||||
|
defaultBurst = 50
|
||||||
|
defaultTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// Config tracks a kubernetes configuration.
|
// Config tracks a kubernetes configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
flags *genericclioptions.ConfigFlags
|
||||||
|
|
@ -280,6 +287,11 @@ func (c *Config) RESTConfig() (*restclient.Config, error) {
|
||||||
if c.restConfig, err = c.flags.ToRESTConfig(); err != nil {
|
if c.restConfig, err = c.flags.ToRESTConfig(); err != nil {
|
||||||
return nil, err
|
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)
|
log.Debug().Msgf("Connecting to API Server %s", c.restConfig.Host)
|
||||||
|
|
||||||
return c.restConfig, nil
|
return c.restConfig, nil
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
// Describe describes a resource.
|
// Describe describes a resource.
|
||||||
func Describe(c client.Connection, gvr client.GVR, path string) (string, error) {
|
func Describe(c client.Connection, gvr client.GVR, path string) (string, error) {
|
||||||
log.Debug().Msgf("DESCRIBE %q::%q", gvr, path)
|
|
||||||
mapper := RestMapper{Connection: c}
|
mapper := RestMapper{Connection: c}
|
||||||
m, err := mapper.ToRESTMapper()
|
m, err := mapper.ToRESTMapper()
|
||||||
if err != nil {
|
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)
|
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
|
// No Deal!
|
||||||
if !ok {
|
pmx, _ := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
|
||||||
log.Warn().Msgf("no metrics available for %q", p.gvr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)}, nil
|
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
|
return oo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
|
// No Deal!
|
||||||
if !ok {
|
pmx, _ := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
|
||||||
log.Warn().Msgf("no metrics available for %q", p.gvr)
|
|
||||||
}
|
|
||||||
|
|
||||||
var res []runtime.Object
|
var res []runtime.Object
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
@ -25,26 +26,24 @@ type (
|
||||||
|
|
||||||
// Cluster represents a kubernetes resource.
|
// Cluster represents a kubernetes resource.
|
||||||
Cluster struct {
|
Cluster struct {
|
||||||
client client.Connection
|
factory dao.Factory
|
||||||
mx MetricsServer
|
mx MetricsServer
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCluster returns a new cluster info resource.
|
// NewCluster returns a new cluster info resource.
|
||||||
func NewCluster(c client.Connection, mx MetricsServer) *Cluster {
|
func NewCluster(f dao.Factory) *Cluster {
|
||||||
return NewClusterWithArgs(c, mx)
|
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.
|
// Version returns the current K8s cluster version.
|
||||||
func (c *Cluster) Version() string {
|
func (c *Cluster) Version() string {
|
||||||
info, err := c.client.ServerVersion()
|
info, err := c.factory.Client().ServerVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "n/a"
|
return NA
|
||||||
}
|
}
|
||||||
|
|
||||||
return info.GitVersion
|
return info.GitVersion
|
||||||
|
|
@ -52,32 +51,42 @@ func (c *Cluster) Version() string {
|
||||||
|
|
||||||
// ContextName returns the context name.
|
// ContextName returns the context name.
|
||||||
func (c *Cluster) ContextName() string {
|
func (c *Cluster) ContextName() string {
|
||||||
n, err := c.client.Config().CurrentContextName()
|
n, err := c.factory.Client().Config().CurrentContextName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "n/a"
|
return NA
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClusterName returns the cluster name.
|
// ClusterName returns the cluster name.
|
||||||
func (c *Cluster) ClusterName() string {
|
func (c *Cluster) ClusterName() string {
|
||||||
n, err := c.client.Config().CurrentClusterName()
|
n, err := c.factory.Client().Config().CurrentClusterName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "n/a"
|
return NA
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserName returns the user name.
|
// UserName returns the user name.
|
||||||
func (c *Cluster) UserName() string {
|
func (c *Cluster) UserName() string {
|
||||||
n, err := c.client.Config().CurrentUserName()
|
n, err := c.factory.Client().Config().CurrentUserName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "n/a"
|
return NA
|
||||||
}
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics gathers node level metrics and compute utilization percentages.
|
// 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)
|
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
|
oo, err = []runtime.Object{o}, e
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Error().Err(err).Msg("Reconcile failed to list resource")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows render.Rows
|
var rows render.Rows
|
||||||
|
|
|
||||||
|
|
@ -61,17 +61,16 @@ func (r RowEvent) Clone() RowEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changed returns true if the row changed.
|
// Diff returns true if the row changed.
|
||||||
func (r RowEvent) Changed(re RowEvent) bool {
|
func (r RowEvent) Diff(re RowEvent) bool {
|
||||||
if r.Kind != re.Kind {
|
if r.Kind != re.Kind {
|
||||||
log.Debug().Msgf("KIND Changed")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(r.Deltas, re.Deltas) {
|
if !reflect.DeepEqual(r.Deltas, re.Deltas) {
|
||||||
log.Debug().Msgf("DELTAS CHANGED")
|
|
||||||
return true
|
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])
|
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.
|
// RowEvents a collection of row events.
|
||||||
type RowEvents []RowEvent
|
type RowEvents []RowEvent
|
||||||
|
|
||||||
// Changed returns true if the header changed.
|
// Diff returns true if the event changed.
|
||||||
func (rr RowEvents) Changed(r RowEvents) bool {
|
func (rr RowEvents) Diff(r RowEvents) bool {
|
||||||
if len(rr) != len(r) {
|
if len(rr) != len(r) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range rr {
|
for i := range rr {
|
||||||
if rr[i].Changed(r[i]) {
|
if rr[i].Diff(r[i]) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ func (hh HeaderRow) Clear() HeaderRow {
|
||||||
return HeaderRow{}
|
return HeaderRow{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changed returns true if the header changed.
|
// Diff returns true if the header changed.
|
||||||
func (hh HeaderRow) Changed(h HeaderRow) bool {
|
func (hh HeaderRow) Diff(h HeaderRow) bool {
|
||||||
if len(hh) != len(h) {
|
if len(hh) != len(h) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,10 @@ func (t *TableData) Diff(table TableData) bool {
|
||||||
if t.Namespace != table.Namespace {
|
if t.Namespace != table.Namespace {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if t.Header.Changed(table.Header) {
|
if t.Header.Diff(table.Header) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if t.RowEvents.Changed(table.RowEvents) {
|
if t.RowEvents.Diff(table.RowEvents) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ const (
|
||||||
// DeltaSign signals a diff.
|
// DeltaSign signals a diff.
|
||||||
DeltaSign = "Δ"
|
DeltaSign = "Δ"
|
||||||
// PlusSign signals inc.
|
// PlusSign signals inc.
|
||||||
PlusSign = "↑"
|
PlusSign = "[red::b]↑"
|
||||||
// MinusSign signal dec.
|
// MinusSign signal dec.
|
||||||
MinusSign = "↓"
|
MinusSign = "[green::b]↓"
|
||||||
)
|
)
|
||||||
|
|
||||||
var percent = regexp.MustCompile(`\A(\d+)\%\z`)
|
var percent = regexp.MustCompile(`\A(\d+)\%\z`)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
)
|
)
|
||||||
|
|
@ -43,6 +45,36 @@ func (s *StatusIndicator) StylesChanged(styles *config.Styles) {
|
||||||
s.SetTextColor(styles.FgColor())
|
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
|
// SetPermanent sets permanent title to be reset to after updates
|
||||||
func (s *StatusIndicator) SetPermanent(info string) {
|
func (s *StatusIndicator) SetPermanent(info string) {
|
||||||
s.permanent = info
|
s.permanent = info
|
||||||
|
|
@ -93,3 +125,14 @@ func (s *StatusIndicator) setText(msg string) {
|
||||||
}
|
}
|
||||||
}(ctx)
|
}(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) {
|
func hotKeyActions(r Runner, aa ui.KeyActions) {
|
||||||
hh := config.NewHotKeys()
|
hh := config.NewHotKeys()
|
||||||
if err := hh.Load(); err != nil {
|
if err := hh.Load(); err != nil {
|
||||||
log.Error().Err(err).Msgf("Loading HOTKEYS")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, hk := range hh.HotKey {
|
for k, hk := range hh.HotKey {
|
||||||
key, err := asKey(hk.ShortCut)
|
key, err := asKey(hk.ShortCut)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
_, ok := aa[key]
|
_, ok := aa[key]
|
||||||
if ok {
|
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
|
continue
|
||||||
}
|
}
|
||||||
aa[key] = ui.NewSharedKeyAction(
|
aa[key] = ui.NewSharedKeyAction(
|
||||||
|
|
@ -86,7 +85,6 @@ func gotoCmd(r Runner, cmd string) ui.ActionHandler {
|
||||||
func pluginActions(r Runner, aa ui.KeyActions) {
|
func pluginActions(r Runner, aa ui.KeyActions) {
|
||||||
pp := config.NewPlugins()
|
pp := config.NewPlugins()
|
||||||
if err := pp.Load(); err != nil {
|
if err := pp.Load(); err != nil {
|
||||||
log.Warn().Msgf("No plugin configuration found")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,12 +94,12 @@ func pluginActions(r Runner, aa ui.KeyActions) {
|
||||||
}
|
}
|
||||||
key, err := asKey(plugin.ShortCut)
|
key, err := asKey(plugin.ShortCut)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
_, ok := aa[key]
|
_, ok := aa[key]
|
||||||
if ok {
|
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
|
continue
|
||||||
}
|
}
|
||||||
aa[key] = ui.NewKeyAction(
|
aa[key] = ui.NewKeyAction(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/model"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/k9s/internal/render"
|
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
"github.com/derailed/k9s/internal/watch"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -22,9 +21,9 @@ import (
|
||||||
var ExitStatus = ""
|
var ExitStatus = ""
|
||||||
|
|
||||||
const (
|
const (
|
||||||
splashTime = 1
|
splashDelay = 1 * time.Second
|
||||||
clusterRefresh = 5 * 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
|
clusterInfoWidth = 50
|
||||||
clusterInfoPad = 15
|
clusterInfoPad = 15
|
||||||
)
|
)
|
||||||
|
|
@ -39,6 +38,8 @@ type App struct {
|
||||||
version string
|
version string
|
||||||
showHeader bool
|
showHeader bool
|
||||||
cancelFn context.CancelFunc
|
cancelFn context.CancelFunc
|
||||||
|
conRetry int
|
||||||
|
clusterModel *model.ClusterInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp returns a K9s app instance.
|
// NewApp returns a K9s app instance.
|
||||||
|
|
@ -51,11 +52,16 @@ func NewApp(cfg *config.Config) *App {
|
||||||
a.InitBench(cfg.K9s.CurrentCluster)
|
a.InitBench(cfg.K9s.CurrentCluster)
|
||||||
|
|
||||||
a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles)
|
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
|
return &a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConOK checks the connection is cool, returns false otherwise.
|
||||||
|
func (a *App) ConOK() bool {
|
||||||
|
return a.conRetry == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Init initializes the application.
|
// Init initializes the application.
|
||||||
func (a *App) Init(version string, rate int) error {
|
func (a *App) Init(version string, rate int) error {
|
||||||
a.version = version
|
a.version = version
|
||||||
|
|
@ -80,15 +86,17 @@ func (a *App) Init(version string, rate int) error {
|
||||||
a.factory = watch.NewFactory(a.Conn())
|
a.factory = watch.NewFactory(a.Conn())
|
||||||
a.initFactory(ns)
|
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)
|
a.command = NewCommand(a)
|
||||||
if err := a.command.Init(); err != nil {
|
if err := a.command.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.clusterInfo().Init(version)
|
a.clusterInfo().Init()
|
||||||
if a.Config.K9s.GetHeadless() {
|
|
||||||
a.refreshIndicator()
|
|
||||||
}
|
|
||||||
|
|
||||||
main := tview.NewFlex().SetDirection(tview.FlexRow)
|
main := tview.NewFlex().SetDirection(tview.FlexRow)
|
||||||
main.AddItem(a.statusIndicator(), 1, 1, false)
|
main.AddItem(a.statusIndicator(), 1, 1, false)
|
||||||
|
|
@ -129,7 +137,6 @@ func (a *App) toggleHeader(flag bool) {
|
||||||
} else {
|
} else {
|
||||||
flex.RemoveItemAtIndex(0)
|
flex.RemoveItemAtIndex(0)
|
||||||
flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)
|
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!")
|
log.Debug().Msg("ClusterInfo updater canceled!")
|
||||||
return
|
return
|
||||||
case <-time.After(clusterRefresh):
|
case <-time.After(clusterRefresh):
|
||||||
a.refreshClusterInfo()
|
a.refreshCluster()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOZO!! Refact to use model/view strategy.
|
func (a *App) refreshCluster() {
|
||||||
func (a *App) refreshClusterInfo() {
|
c := a.Content.Top()
|
||||||
if !a.Conn().CheckConnectivity() {
|
|
||||||
ExitStatus = "Lost K8s connection. Bailing out!"
|
// 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()
|
a.BailOut()
|
||||||
}
|
}
|
||||||
|
if a.conRetry > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Reload alias
|
// Reload alias
|
||||||
|
go func() {
|
||||||
if err := a.command.Reset(); err != nil {
|
if err := a.command.Reset(); err != nil {
|
||||||
log.Error().Err(err).Msgf("Command reset failed")
|
log.Error().Err(err).Msgf("Command reset failed")
|
||||||
}
|
}
|
||||||
a.QueueUpdateDraw(func() {
|
}()
|
||||||
if !a.showHeader {
|
|
||||||
a.refreshIndicator()
|
|
||||||
} else {
|
|
||||||
a.clusterInfo().refresh()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) refreshIndicator() {
|
// Update cluster info
|
||||||
mx := client.NewMetricsServer(a.Conn())
|
a.clusterModel.Refresh()
|
||||||
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,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) switchNS(ns string) bool {
|
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 {
|
if err := a.gotoResource("pods", true); loadPods && err != nil {
|
||||||
a.Flash().Err(err)
|
a.Flash().Err(err)
|
||||||
}
|
}
|
||||||
a.refreshClusterInfo()
|
a.clusterModel.Reset(a.factory)
|
||||||
a.ReloadStyles(name)
|
a.ReloadStyles(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,7 +292,7 @@ func (a *App) Run() error {
|
||||||
a.Resume()
|
a.Resume()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-time.After(splashTime * time.Second)
|
<-time.After(splashDelay)
|
||||||
a.QueueUpdateDraw(func() {
|
a.QueueUpdateDraw(func() {
|
||||||
a.Main.SwitchToPage("main")
|
a.Main.SwitchToPage("main")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (b *Browser) Init(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ns := client.CleanseNamespace(b.app.Config.ActiveNamespace())
|
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 {
|
if _, e := b.app.factory.CanForResource(ns, b.GVR(), client.MonitorAccess); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
@ -92,6 +92,7 @@ func (b *Browser) SetInstance(path string) {
|
||||||
func (b *Browser) Start() {
|
func (b *Browser) Start() {
|
||||||
b.Stop()
|
b.Stop()
|
||||||
|
|
||||||
|
log.Debug().Msgf("BROWSER started!")
|
||||||
b.Table.Start()
|
b.Table.Start()
|
||||||
ctx := b.defaultContext()
|
ctx := b.defaultContext()
|
||||||
ctx, b.cancelFn = context.WithCancel(ctx)
|
ctx, b.cancelFn = context.WithCancel(ctx)
|
||||||
|
|
@ -109,6 +110,7 @@ func (b *Browser) Stop() {
|
||||||
if b.cancelFn == nil {
|
if b.cancelFn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Debug().Msgf("BROWSER Stopped!")
|
||||||
b.Table.Stop()
|
b.Table.Stop()
|
||||||
b.cancelFn()
|
b.cancelFn()
|
||||||
b.cancelFn = nil
|
b.cancelFn = nil
|
||||||
|
|
@ -137,6 +139,10 @@ func (b *Browser) Aliases() []string {
|
||||||
|
|
||||||
// TableDataChanged notifies view new data is available.
|
// TableDataChanged notifies view new data is available.
|
||||||
func (b *Browser) TableDataChanged(data render.TableData) {
|
func (b *Browser) TableDataChanged(data render.TableData) {
|
||||||
|
if !b.app.ConOK() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
b.app.QueueUpdateDraw(func() {
|
b.app.QueueUpdateDraw(func() {
|
||||||
b.refreshActions()
|
b.refreshActions()
|
||||||
b.Update(data)
|
b.Update(data)
|
||||||
|
|
@ -354,30 +360,14 @@ func (b *Browser) defaultContext() context.Context {
|
||||||
return ctx
|
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() {
|
func (b *Browser) refreshActions() {
|
||||||
aa := ui.KeyActions{
|
aa := ui.KeyActions{
|
||||||
ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false),
|
ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false),
|
||||||
tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false),
|
tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false),
|
||||||
tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false),
|
tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.app.ConOK() {
|
||||||
b.namespaceActions(aa)
|
b.namespaceActions(aa)
|
||||||
|
|
||||||
if client.Can(b.meta.Verbs, "edit") {
|
if client.Can(b.meta.Verbs, "edit") {
|
||||||
|
|
@ -386,6 +376,7 @@ func (b *Browser) refreshActions() {
|
||||||
if client.Can(b.meta.Verbs, "delete") {
|
if client.Can(b.meta.Verbs, "delete") {
|
||||||
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true)
|
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !dao.IsK9sMeta(b.meta) {
|
if !dao.IsK9sMeta(b.meta) {
|
||||||
aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true)
|
aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true)
|
||||||
|
|
@ -402,6 +393,24 @@ func (b *Browser) refreshActions() {
|
||||||
b.app.Menu().HydrateMenu(b.Hints())
|
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) {
|
func (b *Browser) simpleDelete(selections []string, msg string) {
|
||||||
dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() {
|
dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() {
|
||||||
b.ShowDeleted()
|
b.ShowDeleted()
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,51 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
|
||||||
"github.com/derailed/k9s/internal/model"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"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.
|
// ClusterInfo represents a cluster info view.
|
||||||
type ClusterInfo struct {
|
type ClusterInfo struct {
|
||||||
*tview.Table
|
*tview.Table
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
mxs *client.MetricsServer
|
|
||||||
styles *config.Styles
|
styles *config.Styles
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClusterInfo returns a new cluster info view.
|
// NewClusterInfo returns a new cluster info view.
|
||||||
func NewClusterInfo(app *App, mx *client.MetricsServer) *ClusterInfo {
|
func NewClusterInfo(app *App) *ClusterInfo {
|
||||||
return &ClusterInfo{
|
return &ClusterInfo{
|
||||||
app: app,
|
|
||||||
Table: tview.NewTable(),
|
Table: tview.NewTable(),
|
||||||
mxs: mx,
|
app: app,
|
||||||
styles: app.Styles,
|
styles: app.Styles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the view.
|
// Init initializes the view.
|
||||||
func (c *ClusterInfo) Init(version string) {
|
func (c *ClusterInfo) Init() {
|
||||||
cluster := model.NewCluster(c.app.Conn(), c.mxs)
|
|
||||||
|
|
||||||
c.app.Styles.AddListener(c)
|
c.app.Styles.AddListener(c)
|
||||||
|
c.layout()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StylesChanged notifies skin changed.
|
// StylesChanged notifies skin changed.
|
||||||
func (c *ClusterInfo) StylesChanged(s *config.Styles) {
|
func (c *ClusterInfo) StylesChanged(s *config.Styles) {
|
||||||
c.styles = s
|
c.styles = s
|
||||||
c.SetBackgroundColor(s.BgColor())
|
c.SetBackgroundColor(s.BgColor())
|
||||||
c.refresh()
|
c.updateStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterInfo) initInfo(cluster *model.Cluster) int {
|
func (c *ClusterInfo) layout() {
|
||||||
var row int
|
for row, v := range []string{"Context", "Cluster", "User", "K9s Rev", "K8s Rev", "CPU", "MEM"} {
|
||||||
c.SetCell(row, 0, c.sectionCell("Context"))
|
c.SetCell(row, 0, c.sectionCell(v))
|
||||||
c.SetCell(row, 1, c.infoCell(cluster.ContextName()))
|
c.SetCell(row, 1, c.infoCell(render.NAValue))
|
||||||
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) sectionCell(t string) *tview.TableCell {
|
func (c *ClusterInfo) sectionCell(t string) *tview.TableCell {
|
||||||
|
|
@ -108,30 +67,46 @@ func (c *ClusterInfo) infoCell(t string) *tview.TableCell {
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterInfo) refresh() {
|
func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) {
|
||||||
var (
|
c.app.QueueUpdateDraw(func() {
|
||||||
cluster = model.NewCluster(c.app.Conn(), c.mxs)
|
var row int
|
||||||
row int
|
c.GetCell(row, 1).SetText(data.Context)
|
||||||
)
|
|
||||||
|
|
||||||
c.GetCell(row, 1).SetText(cluster.ContextName())
|
|
||||||
row++
|
row++
|
||||||
c.GetCell(row, 1).SetText(cluster.ClusterName())
|
c.GetCell(row, 1).SetText(data.Cluster)
|
||||||
row++
|
row++
|
||||||
c.GetCell(row, 1).SetText(cluster.UserName())
|
c.GetCell(row, 1).SetText(data.User)
|
||||||
row += 2
|
|
||||||
c.GetCell(row, 1).SetText(cluster.Version())
|
|
||||||
row++
|
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()
|
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() {
|
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)))
|
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"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
|
|
@ -23,6 +24,7 @@ type Command struct {
|
||||||
app *App
|
app *App
|
||||||
|
|
||||||
alias *dao.Alias
|
alias *dao.Alias
|
||||||
|
mx sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommand returns a new command.
|
// NewCommand returns a new command.
|
||||||
|
|
@ -45,6 +47,9 @@ func (c *Command) Init() error {
|
||||||
|
|
||||||
// Reset resets Command and reload aliases.
|
// Reset resets Command and reload aliases.
|
||||||
func (c *Command) Reset() error {
|
func (c *Command) Reset() error {
|
||||||
|
c.mx.Lock()
|
||||||
|
defer c.mx.Unlock()
|
||||||
|
|
||||||
c.alias.Clear()
|
c.alias.Clear()
|
||||||
if _, err := c.alias.Ensure(); err != nil {
|
if _, err := c.alias.Ensure(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -134,10 +139,12 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) defaultCmd() 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")
|
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 {
|
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 {
|
func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer {
|
||||||
var view ResourceViewer
|
var view ResourceViewer
|
||||||
if v.viewerFn != nil {
|
if v.viewerFn != nil {
|
||||||
log.Debug().Msgf("Custom viewer for %s", gvr)
|
|
||||||
view = v.viewerFn(client.NewGVR(gvr))
|
view = v.viewerFn(client.NewGVR(gvr))
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msgf("Generic viewer for %s", gvr)
|
|
||||||
view = NewBrowser(client.NewGVR(gvr))
|
view = NewBrowser(client.NewGVR(gvr))
|
||||||
}
|
}
|
||||||
|
|
||||||
view.SetInstance(path)
|
view.SetInstance(path)
|
||||||
if v.enterFn != nil {
|
if v.enterFn != nil {
|
||||||
log.Debug().Msgf("SETTING CUSTOM ENTER ON %s", gvr)
|
|
||||||
view.GetTable().SetEnterFn(v.enterFn)
|
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 {
|
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"))
|
res, err := dao.AccessorFor(app.factory, client.NewGVR("contexts"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 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 {
|
func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if p.bench != nil {
|
if p.bench != nil {
|
||||||
p.App().Status(ui.FlashErr, "Benchmark Camceled!")
|
p.App().Status(ui.FlashErr, "Benchmark Canceled!")
|
||||||
p.bench.Cancel()
|
p.bench.Cancel()
|
||||||
p.App().ClearStatus(true)
|
p.App().ClearStatus(true)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue