checkpoint
parent
a76d5809b8
commit
355f4ce396
|
|
@ -16,6 +16,7 @@ builds:
|
|||
- 386
|
||||
- amd64
|
||||
- arm64
|
||||
- armhf
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 731 KiB |
4
go.mod
4
go.mod
|
|
@ -43,14 +43,14 @@ require (
|
|||
github.com/gdamore/tcell v1.3.0
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.5
|
||||
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
||||
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
||||
github.com/openfaas/faas-provider v0.15.0
|
||||
github.com/petergtz/pegomock v2.6.0+incompatible
|
||||
github.com/rakyll/hey v0.1.2
|
||||
github.com/rs/zerolog v1.17.2
|
||||
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1
|
||||
github.com/rs/zerolog v1.18.0
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/spf13/cobra v0.0.5
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -483,6 +483,7 @@ github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwd
|
|||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
|
||||
github.com/mum4k/termdash v0.10.0/go.mod h1:l3tO+lJi9LZqXRq7cu7h5/8rDIK3AzelSuq2v/KncxI=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
||||
|
|
@ -565,6 +566,7 @@ github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H
|
|||
github.com/rakyll/hey v0.1.2 h1:XlGaKcBdmXJaPImiTnE+TGLDUWQ2toYuHCwdrylLjmg=
|
||||
github.com/rakyll/hey v0.1.2/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1 h1:s9Lw4phBWkuQJUd+msaBMxP3utLvrFaBQV9jNgG55r0=
|
||||
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1/go.mod h1:+rKjP5+h9HMwWRpAfhIkkQ9KE3m3Nz5rwn7YtUpwgqk=
|
||||
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
|
|
@ -575,6 +577,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
|||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
|
||||
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
|
||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
|
|
|
|||
|
|
@ -3,20 +3,46 @@ package client
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/cache"
|
||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
mxCacheSize = 100
|
||||
mxCacheExpiry = 1 * time.Minute
|
||||
)
|
||||
|
||||
var MetricsDial *MetricsServer
|
||||
|
||||
func DialMetrics(c Connection) *MetricsServer {
|
||||
if MetricsDial == nil {
|
||||
MetricsDial = NewMetricsServer(c)
|
||||
}
|
||||
|
||||
return MetricsDial
|
||||
}
|
||||
|
||||
func ResetMetrics() {
|
||||
MetricsDial = nil
|
||||
}
|
||||
|
||||
// MetricsServer serves cluster metrics for nodes and pods.
|
||||
type MetricsServer struct {
|
||||
Connection
|
||||
|
||||
cache *cache.LRUExpireCache
|
||||
}
|
||||
|
||||
// NewMetricsServer return a metric server instance.
|
||||
func NewMetricsServer(c Connection) *MetricsServer {
|
||||
return &MetricsServer{Connection: c}
|
||||
return &MetricsServer{
|
||||
Connection: c,
|
||||
cache: cache.NewLRUExpireCache(mxCacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
// NodesMetrics retrieves metrics for a given set of nodes.
|
||||
|
|
@ -28,15 +54,15 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
|
|||
for _, no := range nodes.Items {
|
||||
mmx[no.Name] = NodeMetrics{
|
||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
||||
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
|
||||
AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()),
|
||||
TotalCPU: no.Status.Capacity.Cpu().MilliValue(),
|
||||
TotalMEM: toMB(no.Status.Capacity.Memory().Value()),
|
||||
TotalMEM: ToMB(no.Status.Capacity.Memory().Value()),
|
||||
}
|
||||
}
|
||||
for _, c := range metrics.Items {
|
||||
if mx, ok := mmx[c.Name]; ok {
|
||||
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
||||
mx.CurrentMEM = toMB(c.Usage.Memory().Value())
|
||||
mx.CurrentMEM = ToMB(c.Usage.Memory().Value())
|
||||
mmx[c.Name] = mx
|
||||
}
|
||||
}
|
||||
|
|
@ -51,13 +77,13 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
for _, no := range nos.Items {
|
||||
nodeMetrics[no.Name] = NodeMetrics{
|
||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
||||
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
|
||||
AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()),
|
||||
}
|
||||
}
|
||||
for _, mx := range nmx.Items {
|
||||
if m, ok := nodeMetrics[mx.Name]; ok {
|
||||
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
||||
m.CurrentMEM = toMB(mx.Usage.Memory().Value())
|
||||
m.CurrentMEM = ToMB(mx.Usage.Memory().Value())
|
||||
nodeMetrics[mx.Name] = m
|
||||
}
|
||||
}
|
||||
|
|
@ -74,86 +100,121 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
return nil
|
||||
}
|
||||
|
||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||
var mx mv1beta1.NodeMetricsList
|
||||
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
return fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
|
||||
auth, err := m.CanI("", "metrics.k8s.io/v1beta1/nodes", ListAccess)
|
||||
auth, err := m.CanI(ns, gvr, ListAccess)
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return &mx, fmt.Errorf("user is not authorized to list node metrics")
|
||||
return fmt.Errorf(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchNodesMetrics return all metrics for nodes.
|
||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||
const msg = "user is not authorized to list node metrics"
|
||||
|
||||
mx := new(mv1beta1.NodeMetricsList)
|
||||
if err := m.checkAccess("", "metrics.k8s.io/v1beta1/nodes", msg); err != nil {
|
||||
return mx, err
|
||||
}
|
||||
|
||||
const key = "nodes"
|
||||
if entry, ok := m.cache.Get(key); ok && entry != nil {
|
||||
mxList, ok := entry.(*mv1beta1.NodeMetricsList)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected nodemetricslist but got %T", entry)
|
||||
}
|
||||
return mxList, nil
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
return mx, err
|
||||
}
|
||||
return client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
||||
mxList, err := client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return mx, err
|
||||
}
|
||||
m.cache.Add(key, mxList, mxCacheExpiry)
|
||||
|
||||
return mxList, nil
|
||||
}
|
||||
|
||||
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||
var mx mv1beta1.PodMetricsList
|
||||
if m.Connection == nil {
|
||||
return &mx, fmt.Errorf("no client connection")
|
||||
}
|
||||
mx := new(mv1beta1.PodMetricsList)
|
||||
const msg = "user is not authorized to list pods metrics"
|
||||
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
if ns == NamespaceAll {
|
||||
ns = AllNamespaces
|
||||
}
|
||||
|
||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", ListAccess)
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||
return mx, err
|
||||
}
|
||||
if !auth {
|
||||
return &mx, fmt.Errorf("user is not authorized to list pods metrics")
|
||||
|
||||
key := FQN(ns, "pods")
|
||||
if entry, ok := m.cache.Get(key); ok {
|
||||
mxList, ok := entry.(*mv1beta1.PodMetricsList)
|
||||
if !ok {
|
||||
return mx, fmt.Errorf("expected podmetricslist but got %T", entry)
|
||||
}
|
||||
return mxList, nil
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
return mx, err
|
||||
}
|
||||
mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return mx, err
|
||||
}
|
||||
m.cache.Add(key, mxList, mxCacheExpiry)
|
||||
|
||||
return client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
|
||||
return mxList, err
|
||||
}
|
||||
|
||||
// FetchPodMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
|
||||
var mx mv1beta1.PodMetrics
|
||||
if m.Connection == nil {
|
||||
return &mx, fmt.Errorf("no client connection")
|
||||
}
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
var mx *mv1beta1.PodMetrics
|
||||
const msg = "user is not authorized to list pod metrics"
|
||||
|
||||
ns, n := Namespaced(fqn)
|
||||
if ns == NamespaceAll {
|
||||
ns = AllNamespaces
|
||||
}
|
||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", GetAccess)
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||
return mx, err
|
||||
}
|
||||
if !auth {
|
||||
return &mx, fmt.Errorf("user is not authorized to list pod metrics")
|
||||
|
||||
var key = FQN(ns, "pods")
|
||||
if entry, ok := m.cache.Get(key); ok {
|
||||
if list, ok := entry.(*mv1beta1.PodMetricsList); ok && list != nil {
|
||||
for _, m := range list.Items {
|
||||
if FQN(m.Namespace, m.Name) == fqn {
|
||||
return &m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
return mx, err
|
||||
}
|
||||
mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return mx, err
|
||||
}
|
||||
m.cache.Add(key, mx, mxCacheExpiry)
|
||||
|
||||
return client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
|
||||
return mx, nil
|
||||
}
|
||||
|
||||
// PodsMetrics retrieves metrics for all pods in a given namespace.
|
||||
|
|
@ -167,7 +228,7 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
|||
var mx PodMetrics
|
||||
for _, c := range p.Containers {
|
||||
mx.CurrentCPU += c.Usage.Cpu().MilliValue()
|
||||
mx.CurrentMEM += toMB(c.Usage.Memory().Value())
|
||||
mx.CurrentMEM += ToMB(c.Usage.Memory().Value())
|
||||
}
|
||||
mmx[p.Namespace+"/"+p.Name] = mx
|
||||
}
|
||||
|
|
@ -178,8 +239,8 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
|||
|
||||
const megaByte = 1024 * 1024
|
||||
|
||||
// toMB converts bytes to megabytes.
|
||||
func toMB(v int64) float64 {
|
||||
// ToMB converts bytes to megabytes.
|
||||
func ToMB(v int64) float64 {
|
||||
return float64(v) / megaByte
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const (
|
|||
|
||||
// ClusterScope designates a resource is not namespaced.
|
||||
ClusterScope = "-"
|
||||
|
||||
// NotNamespaced designates a non resource namespace.
|
||||
NotNamespaced = "*"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -31,65 +31,6 @@ func NewAliases() *Aliases {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *Aliases) loadDefaults() {
|
||||
const (
|
||||
contexts = "contexts"
|
||||
portFwds = "portforwards"
|
||||
benchmarks = "benchmarks"
|
||||
dumps = "screendumps"
|
||||
groups = "groups"
|
||||
users = "users"
|
||||
)
|
||||
|
||||
a.mx.Lock()
|
||||
defer a.mx.Unlock()
|
||||
|
||||
a.Alias["dp"] = "apps/v1/deployments"
|
||||
a.Alias["sec"] = "v1/secrets"
|
||||
a.Alias["jo"] = "batch/v1/jobs"
|
||||
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
|
||||
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
|
||||
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
|
||||
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
||||
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
||||
{
|
||||
a.Alias["ctx"] = contexts
|
||||
a.Alias[contexts] = contexts
|
||||
a.Alias["context"] = contexts
|
||||
}
|
||||
{
|
||||
a.Alias["usr"] = users
|
||||
a.Alias[users] = users
|
||||
a.Alias["user"] = users
|
||||
}
|
||||
{
|
||||
a.Alias["grp"] = groups
|
||||
a.Alias["group"] = groups
|
||||
a.Alias[groups] = groups
|
||||
}
|
||||
{
|
||||
a.Alias["pf"] = portFwds
|
||||
a.Alias[portFwds] = portFwds
|
||||
a.Alias["portforward"] = portFwds
|
||||
}
|
||||
{
|
||||
a.Alias["be"] = benchmarks
|
||||
a.Alias["benchmark"] = benchmarks
|
||||
a.Alias[benchmarks] = benchmarks
|
||||
}
|
||||
{
|
||||
a.Alias["sd"] = dumps
|
||||
a.Alias["screendump"] = dumps
|
||||
a.Alias[dumps] = dumps
|
||||
}
|
||||
}
|
||||
|
||||
// Load K9s aliases.
|
||||
func (a *Aliases) Load() error {
|
||||
a.loadDefaults()
|
||||
return a.LoadAliases(K9sAlias)
|
||||
}
|
||||
|
||||
// ShortNames return all shortnames.
|
||||
func (a *Aliases) ShortNames() ShortNames {
|
||||
a.mx.RLock()
|
||||
|
|
@ -139,8 +80,14 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
|
|||
}
|
||||
}
|
||||
|
||||
// LoadAliases loads alias from a given file.
|
||||
func (a *Aliases) LoadAliases(path string) error {
|
||||
// Load K9s aliases.
|
||||
func (a *Aliases) Load() error {
|
||||
a.loadDefaultAliases()
|
||||
return a.LoadFileAliases(K9sAlias)
|
||||
}
|
||||
|
||||
// LoadFileAliases loads alias from a given file.
|
||||
func (a *Aliases) LoadFileAliases(path string) error {
|
||||
f, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msgf("No custom aliases found")
|
||||
|
|
@ -161,6 +108,63 @@ func (a *Aliases) LoadAliases(path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *Aliases) loadDefaultAliases() {
|
||||
a.mx.Lock()
|
||||
defer a.mx.Unlock()
|
||||
|
||||
a.Alias["dp"] = "apps/v1/deployments"
|
||||
a.Alias["sec"] = "v1/secrets"
|
||||
a.Alias["jo"] = "batch/v1/jobs"
|
||||
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
|
||||
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
|
||||
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
|
||||
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
||||
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
||||
|
||||
const contexts = "contexts"
|
||||
{
|
||||
a.Alias["ctx"] = contexts
|
||||
a.Alias[contexts] = contexts
|
||||
a.Alias["context"] = contexts
|
||||
}
|
||||
const users = "users"
|
||||
{
|
||||
a.Alias["usr"] = users
|
||||
a.Alias[users] = users
|
||||
a.Alias["user"] = users
|
||||
}
|
||||
const groups = "groups"
|
||||
{
|
||||
a.Alias["grp"] = groups
|
||||
a.Alias["group"] = groups
|
||||
a.Alias[groups] = groups
|
||||
}
|
||||
const portFwds = "portforwards"
|
||||
{
|
||||
a.Alias["pf"] = portFwds
|
||||
a.Alias[portFwds] = portFwds
|
||||
a.Alias["portforward"] = portFwds
|
||||
}
|
||||
const benchmarks = "benchmarks"
|
||||
{
|
||||
a.Alias["be"] = benchmarks
|
||||
a.Alias["benchmark"] = benchmarks
|
||||
a.Alias[benchmarks] = benchmarks
|
||||
}
|
||||
const dumps = "screendumps"
|
||||
{
|
||||
a.Alias["sd"] = dumps
|
||||
a.Alias["screendump"] = dumps
|
||||
a.Alias[dumps] = dumps
|
||||
}
|
||||
const pulses = "pulses"
|
||||
{
|
||||
a.Alias["hz"] = pulses
|
||||
a.Alias["pu"] = pulses
|
||||
a.Alias["pulse"] = pulses
|
||||
}
|
||||
}
|
||||
|
||||
// Save alias to disk.
|
||||
func (a *Aliases) Save() error {
|
||||
log.Debug().Msg("[Config] Saving Aliases...")
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func TestAliasDefine(t *testing.T) {
|
|||
func TestAliasesLoad(t *testing.T) {
|
||||
a := config.NewAliases()
|
||||
|
||||
assert.Nil(t, a.LoadAliases("testdata/alias.yml"))
|
||||
assert.Nil(t, a.LoadFileAliases("testdata/alias.yml"))
|
||||
assert.Equal(t, 2, len(a.Alias))
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +82,6 @@ func TestAliasesSave(t *testing.T) {
|
|||
a.Alias["blee"] = "duh"
|
||||
|
||||
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
|
||||
assert.Nil(t, a.LoadAliases("/tmp/a.yml"))
|
||||
assert.Nil(t, a.LoadFileAliases("/tmp/a.yml"))
|
||||
assert.Equal(t, 2, len(a.Alias))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,31 @@ type StyleListener interface {
|
|||
}
|
||||
|
||||
type (
|
||||
// Color represents a color.
|
||||
Color string
|
||||
|
||||
// Colors tracks multiple colors.
|
||||
Colors []Color
|
||||
|
||||
// Styles tracks K9s styling options.
|
||||
Styles struct {
|
||||
K9s Style `yaml:"k9s"`
|
||||
listeners []StyleListener
|
||||
}
|
||||
|
||||
// Style tracks K9s styles.
|
||||
Style struct {
|
||||
Body Body `yaml:"body"`
|
||||
Frame Frame `yaml:"frame"`
|
||||
Info Info `yaml:"info"`
|
||||
Views Views `yaml:"views"`
|
||||
}
|
||||
|
||||
// Body tracks body styles.
|
||||
Body struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
LogoColor string `yaml:"logoColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
LogoColor Color `yaml:"logoColor"`
|
||||
}
|
||||
|
||||
// Frame tracks frame styles.
|
||||
|
|
@ -45,120 +59,171 @@ type (
|
|||
|
||||
// Views tracks individual view styles.
|
||||
Views struct {
|
||||
Yaml Yaml `yaml:"yaml"`
|
||||
Log Log `yaml:"logs"`
|
||||
Table Table `yaml:"table"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Charts Charts `yaml:"charts"`
|
||||
Yaml Yaml `yaml:"yaml"`
|
||||
Log Log `yaml:"logs"`
|
||||
}
|
||||
|
||||
// Status tracks resource status styles.
|
||||
Status struct {
|
||||
NewColor string `yaml:"newColor"`
|
||||
ModifyColor string `yaml:"modifyColor"`
|
||||
AddColor string `yaml:"addColor"`
|
||||
ErrorColor string `yaml:"errorColor"`
|
||||
HighlightColor string `yaml:"highlightColor"`
|
||||
KillColor string `yaml:"killColor"`
|
||||
CompletedColor string `yaml:"completedColor"`
|
||||
NewColor Color `yaml:"newColor"`
|
||||
ModifyColor Color `yaml:"modifyColor"`
|
||||
AddColor Color `yaml:"addColor"`
|
||||
ErrorColor Color `yaml:"errorColor"`
|
||||
HighlightColor Color `yaml:"highlightColor"`
|
||||
KillColor Color `yaml:"killColor"`
|
||||
CompletedColor Color `yaml:"completedColor"`
|
||||
}
|
||||
|
||||
// Log tracks Log styles.
|
||||
Log struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
}
|
||||
|
||||
// Yaml tracks yaml styles.
|
||||
Yaml struct {
|
||||
KeyColor string `yaml:"keyColor"`
|
||||
ValueColor string `yaml:"valueColor"`
|
||||
ColonColor string `yaml:"colonColor"`
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
ValueColor Color `yaml:"valueColor"`
|
||||
ColonColor Color `yaml:"colonColor"`
|
||||
}
|
||||
|
||||
// Title tracks title styles.
|
||||
Title struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
HighlightColor string `yaml:"highlightColor"`
|
||||
CounterColor string `yaml:"counterColor"`
|
||||
FilterColor string `yaml:"filterColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
HighlightColor Color `yaml:"highlightColor"`
|
||||
CounterColor Color `yaml:"counterColor"`
|
||||
FilterColor Color `yaml:"filterColor"`
|
||||
}
|
||||
|
||||
// Info tracks info styles.
|
||||
Info struct {
|
||||
SectionColor string `yaml:"sectionColor"`
|
||||
FgColor string `yaml:"fgColor"`
|
||||
SectionColor Color `yaml:"sectionColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
}
|
||||
|
||||
// Border tracks border styles.
|
||||
// ColorBorder tracks border styles.
|
||||
Border struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
FocusColor string `yaml:"focusColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
FocusColor Color `yaml:"focusColor"`
|
||||
}
|
||||
|
||||
// Crumb tracks crumbs styles.
|
||||
Crumb struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
ActiveColor string `yaml:"activeColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
ActiveColor Color `yaml:"activeColor"`
|
||||
}
|
||||
|
||||
// Table tracks table styles.
|
||||
Table struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
CursorColor string `yaml:"cursorColor"`
|
||||
MarkColor string `yaml:"markColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
CursorColor Color `yaml:"cursorColor"`
|
||||
MarkColor Color `yaml:"markColor"`
|
||||
Header TableHeader `yaml:"header"`
|
||||
}
|
||||
|
||||
// TableHeader tracks table header styles.
|
||||
TableHeader struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
SorterColor string `yaml:"sorterColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
SorterColor Color `yaml:"sorterColor"`
|
||||
}
|
||||
|
||||
// Xray tracks xray styles.
|
||||
Xray struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
CursorColor string `yaml:"cursorColor"`
|
||||
GraphicColor string `yaml:"graphicColor"`
|
||||
ShowIcons bool `yaml:"showIcons"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
CursorColor Color `yaml:"cursorColor"`
|
||||
GraphicColor Color `yaml:"graphicColor"`
|
||||
ShowIcons bool `yaml:"showIcons"`
|
||||
}
|
||||
|
||||
// Menu tracks menu styles.
|
||||
Menu struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
KeyColor string `yaml:"keyColor"`
|
||||
NumKeyColor string `yaml:"numKeyColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
NumKeyColor Color `yaml:"numKeyColor"`
|
||||
}
|
||||
|
||||
// Style tracks K9s styles.
|
||||
Style struct {
|
||||
Body Body `yaml:"body"`
|
||||
Frame Frame `yaml:"frame"`
|
||||
Info Info `yaml:"info"`
|
||||
Table Table `yaml:"table"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Views Views `yaml:"views"`
|
||||
// Charts tracks charts styles.
|
||||
Charts struct {
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
DialBgColor Color `yaml:"dialBgColor"`
|
||||
ChartBgColor Color `yaml:"chartBgColor"`
|
||||
DefaultDialColors Colors `yaml:"defaultDialColors"`
|
||||
DefaultChartColors Colors `yaml:"defaultChartColors"`
|
||||
ResourceColors map[string]Colors `yaml:"resourceColors"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultColor represents a default color.
|
||||
DefaultColor Color = "default"
|
||||
|
||||
// TransparentColor represents the terminal bg color.
|
||||
TransparentColor Color = "-"
|
||||
)
|
||||
|
||||
// NewColor returns a new color.
|
||||
func NewColor(c string) Color {
|
||||
return Color(c)
|
||||
}
|
||||
|
||||
// String returns color as string.
|
||||
func (c Color) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// AsColor returns a view color.
|
||||
func (c Color) Color() tcell.Color {
|
||||
if c == DefaultColor {
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
if color, ok := tcell.ColorNames[c.String()]; ok {
|
||||
return color
|
||||
}
|
||||
return tcell.GetColor(c.String())
|
||||
}
|
||||
|
||||
// AsColors converts series string colors to colors.
|
||||
func (c Colors) Colors() []tcell.Color {
|
||||
cc := make([]tcell.Color, 0, len(c))
|
||||
for _, color := range c {
|
||||
cc = append(cc, color.Color())
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func newStyle() Style {
|
||||
return Style{
|
||||
Body: newBody(),
|
||||
Frame: newFrame(),
|
||||
Info: newInfo(),
|
||||
Table: newTable(),
|
||||
Views: newViews(),
|
||||
Xray: newXray(),
|
||||
}
|
||||
}
|
||||
|
||||
func newCharts() Charts {
|
||||
return Charts{
|
||||
BgColor: "#111111",
|
||||
DialBgColor: "#111111",
|
||||
ChartBgColor: "#111111",
|
||||
DefaultDialColors: Colors{Color("palegreen"), Color("orangered")},
|
||||
DefaultChartColors: Colors{Color("palegreen"), Color("orangered")},
|
||||
}
|
||||
}
|
||||
func newViews() Views {
|
||||
return Views{
|
||||
Yaml: newYaml(),
|
||||
Log: newLog(),
|
||||
Table: newTable(),
|
||||
Xray: newXray(),
|
||||
Charts: newCharts(),
|
||||
Yaml: newYaml(),
|
||||
Log: newLog(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +253,7 @@ func newStatus() Status {
|
|||
ErrorColor: "orangered",
|
||||
HighlightColor: "aqua",
|
||||
KillColor: "mediumpurple",
|
||||
CompletedColor: "gray",
|
||||
CompletedColor: "lightgray",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -292,6 +357,11 @@ func NewStyles() *Styles {
|
|||
}
|
||||
}
|
||||
|
||||
// Reset resets styles.
|
||||
func (s *Styles) Reset() {
|
||||
s.K9s = newStyle()
|
||||
}
|
||||
|
||||
// DefaultSkin loads the default skin
|
||||
func (s *Styles) DefaultSkin() {
|
||||
s.K9s = newStyle()
|
||||
|
|
@ -299,12 +369,12 @@ func (s *Styles) DefaultSkin() {
|
|||
|
||||
// FgColor returns the foreground color.
|
||||
func (s *Styles) FgColor() tcell.Color {
|
||||
return AsColor(s.Body().FgColor)
|
||||
return s.Body().FgColor.Color()
|
||||
}
|
||||
|
||||
// BgColor returns the background color.
|
||||
func (s *Styles) BgColor() tcell.Color {
|
||||
return AsColor(s.Body().BgColor)
|
||||
return s.Body().BgColor.Color()
|
||||
}
|
||||
|
||||
// AddListener registers a new listener.
|
||||
|
|
@ -353,14 +423,19 @@ func (s *Styles) Title() Title {
|
|||
return s.Frame().Title
|
||||
}
|
||||
|
||||
// Charts returns charts styles.
|
||||
func (s *Styles) Charts() Charts {
|
||||
return s.K9s.Views.Charts
|
||||
}
|
||||
|
||||
// Table returns table styles.
|
||||
func (s *Styles) Table() Table {
|
||||
return s.K9s.Table
|
||||
return s.K9s.Views.Table
|
||||
}
|
||||
|
||||
// Xray returns xray styles.
|
||||
func (s *Styles) Xray() Xray {
|
||||
return s.K9s.Xray
|
||||
return s.K9s.Views.Xray
|
||||
}
|
||||
|
||||
// Views returns views styles.
|
||||
|
|
@ -388,19 +463,7 @@ func (s *Styles) Update() {
|
|||
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
||||
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
||||
tview.Styles.PrimaryTextColor = s.FgColor()
|
||||
tview.Styles.BorderColor = AsColor(s.K9s.Frame.Border.FgColor)
|
||||
tview.Styles.FocusColor = AsColor(s.K9s.Frame.Border.FocusColor)
|
||||
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
||||
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
||||
s.fireStylesChanged()
|
||||
}
|
||||
|
||||
// AsColor checks color index, if match return color otherwise pink it is.
|
||||
func AsColor(c string) tcell.Color {
|
||||
if c == "default" {
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
if color, ok := tcell.ColorNames[c]; ok {
|
||||
return color
|
||||
}
|
||||
|
||||
return tcell.GetColor(c)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAsColor(t *testing.T) {
|
||||
func TestColor(t *testing.T) {
|
||||
uu := map[string]tcell.Color{
|
||||
"blah": tcell.ColorDefault,
|
||||
"blue": tcell.ColorBlue,
|
||||
|
|
@ -20,7 +20,7 @@ func TestAsColor(t *testing.T) {
|
|||
for k := range uu {
|
||||
c, u := k, uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u, config.AsColor(c))
|
||||
assert.Equal(t, u, config.NewColor(c).Color())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -30,9 +30,9 @@ func TestSkinNone(t *testing.T) {
|
|||
assert.Nil(t, s.Load("testdata/empty_skin.yml"))
|
||||
s.Update()
|
||||
|
||||
assert.Equal(t, "cadetblue", s.Body().FgColor)
|
||||
assert.Equal(t, "black", s.Body().BgColor)
|
||||
assert.Equal(t, "black", s.Table().BgColor)
|
||||
assert.Equal(t, "cadetblue", s.Body().FgColor.String())
|
||||
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
||||
|
|
@ -43,9 +43,9 @@ func TestSkin(t *testing.T) {
|
|||
assert.Nil(t, s.Load("testdata/black_and_wtf.yml"))
|
||||
s.Update()
|
||||
|
||||
assert.Equal(t, "white", s.Body().FgColor)
|
||||
assert.Equal(t, "black", s.Body().BgColor)
|
||||
assert.Equal(t, "black", s.Table().BgColor)
|
||||
assert.Equal(t, "white", s.Body().FgColor.String())
|
||||
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
||||
|
|
|
|||
|
|
@ -33,23 +33,21 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("no context path for %q", c.gvr)
|
||||
}
|
||||
|
||||
var (
|
||||
pmx *mv1beta1.PodMetrics
|
||||
err error
|
||||
)
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(fqn); err != nil {
|
||||
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
|
||||
}
|
||||
}
|
||||
|
||||
po, err := c.fetchPod(fqn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pmx *mv1beta1.PodMetrics
|
||||
if c.Client().HasMetrics() {
|
||||
mx := client.NewMetricsServer(c.Client())
|
||||
if c.Client() != nil {
|
||||
var err error
|
||||
pmx, err = mx.FetchPodMetrics(fqn)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
|
||||
for _, co := range po.Spec.InitContainers {
|
||||
res = append(res, makeContainerRes(co, po, pmx, true))
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ type Deployment struct {
|
|||
Resource
|
||||
}
|
||||
|
||||
// IsHappy check for happy deployments.
|
||||
func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
|
||||
return dp.Status.Replicas == dp.Status.AvailableReplicas
|
||||
}
|
||||
|
||||
// Scale a Deployment.
|
||||
func (d *Deployment) Scale(path string, replicas int32) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ type DaemonSet struct {
|
|||
Resource
|
||||
}
|
||||
|
||||
// IsHappy check for happy deployments.
|
||||
func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
|
||||
return ds.Status.DesiredNumberScheduled == ds.Status.CurrentNumberScheduled
|
||||
}
|
||||
|
||||
// Restart a DaemonSet rollout.
|
||||
func (d *DaemonSet) Restart(path string) error {
|
||||
ds, err := d.GetInstance(path)
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type Healthz struct {
|
||||
Resource string
|
||||
Count int64
|
||||
Errors int64
|
||||
}
|
||||
|
||||
type Pulse interface{}
|
||||
type Pulses []Pulse
|
||||
|
||||
|
||||
func ClusterHealth(ctx context.Context, gvr string) (Pulses, error) {
|
||||
var h Healthz
|
||||
|
||||
oo, err := p.List(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.Count = len(oo)
|
||||
for _, o := range oo {
|
||||
var pod v1.Pod
|
||||
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !happy(pod) {
|
||||
h.Errors++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func happy(p v1.Pod) bool {
|
||||
|
||||
}
|
||||
|
|
@ -34,10 +34,14 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
log.Warn().Msgf("No label selector found in context")
|
||||
}
|
||||
|
||||
mx := client.NewMetricsServer(n.Client())
|
||||
nmx, err := mx.FetchNodesMetrics()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No node metrics")
|
||||
var (
|
||||
nmx *mv1beta1.NodeMetricsList
|
||||
err error
|
||||
)
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(); err != nil {
|
||||
log.Warn().Err(err).Msgf("No node metrics")
|
||||
}
|
||||
}
|
||||
|
||||
nn, err := FetchNodes(n.Factory, labels)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ type Pod struct {
|
|||
Resource
|
||||
}
|
||||
|
||||
// IsHappy check for happy deployments.
|
||||
func (p *Pod) IsHappy(po v1.Pod) bool {
|
||||
for _, c := range po.Status.Conditions {
|
||||
if c.Status == v1.ConditionFalse {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Get returns a resource instance if found, else an error.
|
||||
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||
o, err := p.Resource.Get(ctx, path)
|
||||
|
|
@ -50,11 +60,11 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
|||
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
|
||||
}
|
||||
|
||||
// No Deal!
|
||||
mx := client.NewMetricsServer(p.Client())
|
||||
pmx, err := mx.FetchPodMetrics(path)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
var pmx *mv1beta1.PodMetrics
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
|
||||
log.Warn().Err(err).Msgf("No pod metrics")
|
||||
}
|
||||
}
|
||||
|
||||
return &render.PodWithMetrics{Raw: u, MX: pmx}, nil
|
||||
|
|
@ -77,10 +87,11 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
return oo, err
|
||||
}
|
||||
|
||||
mx := client.NewMetricsServer(p.Client())
|
||||
pmx, err := mx.FetchPodsMetrics(ns)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
var pmx *mv1beta1.PodMetricsList
|
||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
|
||||
log.Warn().Err(err).Msgf("No pods metrics")
|
||||
}
|
||||
}
|
||||
|
||||
var res []runtime.Object
|
||||
|
|
@ -128,7 +139,7 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
cc := []string{}
|
||||
cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
|
||||
for _, c := range pod.Spec.Containers {
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type Pulse struct {
|
||||
NonResource
|
||||
}
|
||||
|
||||
func (h *Pulse) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||
return nil, fmt.Errorf("NYI")
|
||||
}
|
||||
|
|
@ -142,6 +142,13 @@ func loadNonResource(m ResourceMetas) {
|
|||
}
|
||||
|
||||
func loadK9s(m ResourceMetas) {
|
||||
m[client.NewGVR("pulses")] = metav1.APIResource{
|
||||
Name: "pulses",
|
||||
Kind: "Pulse",
|
||||
SingularName: "pulses",
|
||||
ShortNames: []string{"hz", "pu"},
|
||||
Categories: []string{"k9s"},
|
||||
}
|
||||
m[client.NewGVR("xrays")] = metav1.APIResource{
|
||||
Name: "xray",
|
||||
Kind: "XRays",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
)
|
||||
|
||||
// ReplicaSet represents a replicaset K8s resource.
|
||||
type ReplicaSet struct {
|
||||
Resource
|
||||
}
|
||||
|
||||
// IsHappy check for happy deployments.
|
||||
func (d *ReplicaSet) IsHappy(rs appsv1.ReplicaSet) bool {
|
||||
if rs.Status.Replicas == 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||
return false
|
||||
}
|
||||
|
||||
if rs.Status.Replicas != 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -29,6 +29,11 @@ type StatefulSet struct {
|
|||
Resource
|
||||
}
|
||||
|
||||
// IsHappy check for happy sts.
|
||||
func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
|
||||
return sts.Status.Replicas == sts.Status.ReadyReplicas
|
||||
}
|
||||
|
||||
// Scale a StatefulSet.
|
||||
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// Check tracks resource health.
|
||||
type Check struct {
|
||||
Counts
|
||||
|
||||
GVR string
|
||||
}
|
||||
|
||||
// Checks represents a collection of health checks.
|
||||
type Checks []*Check
|
||||
|
||||
// NewCheck returns a new health check.
|
||||
func NewCheck(gvr string) *Check {
|
||||
return &Check{
|
||||
GVR: gvr,
|
||||
Counts: make(Counts),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets a health metric.
|
||||
func (c *Check) Set(l Level, v int) {
|
||||
c.Counts[l] = v
|
||||
}
|
||||
|
||||
// Inc increments a health metric.
|
||||
func (c *Check) Inc(l Level) {
|
||||
c.Counts[l]++
|
||||
}
|
||||
|
||||
// Total stores a metric total.
|
||||
func (c *Check) Total(n int) {
|
||||
c.Counts[Corpus] = n
|
||||
}
|
||||
|
||||
// Tally retrieves a given health metric.
|
||||
func (c *Check) Tally(l Level) int {
|
||||
return c.Counts[l]
|
||||
}
|
||||
|
||||
// GetObjectKind returns a schema object.
|
||||
func (Check) GetObjectKind() schema.ObjectKind {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyObject returns a container copy.
|
||||
func (c Check) DeepCopyObject() runtime.Object {
|
||||
return c
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package health_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/health"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
var cc health.Checks
|
||||
|
||||
c := health.NewCheck("test")
|
||||
n := 0
|
||||
for i := 0; i < 10; i++ {
|
||||
c.Inc(health.OK)
|
||||
cc = append(cc, c)
|
||||
n++
|
||||
}
|
||||
c.Total(n)
|
||||
|
||||
assert.Equal(t, 10, len(cc))
|
||||
assert.Equal(t, 10, c.Tally(health.Corpus))
|
||||
assert.Equal(t, 10, c.Tally(health.OK))
|
||||
assert.Equal(t, 0, c.Tally(health.Toast))
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package health
|
||||
|
||||
// Level tracks health count categories.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// Unknown represents no health level.
|
||||
Unknown Level = 1 << iota
|
||||
|
||||
// Corpus tracks total health.
|
||||
Corpus
|
||||
|
||||
// OK tracks healhy.
|
||||
OK
|
||||
|
||||
// Warn tracks health warnings.
|
||||
Warn
|
||||
|
||||
// Toast tracks unhealties.
|
||||
Toast
|
||||
)
|
||||
|
||||
// Message represents a health message.
|
||||
type Message struct {
|
||||
Level Level
|
||||
Message string
|
||||
GVR string
|
||||
FQN string
|
||||
}
|
||||
|
||||
// Messages tracks a collection of messages.
|
||||
type Messages []Message
|
||||
|
||||
// Counts tracks health counts by category.
|
||||
type Counts map[Level]int
|
||||
|
||||
// Vital tracks a resource vitals.
|
||||
type Vital struct {
|
||||
Resource string
|
||||
Total, OK, Toast int
|
||||
}
|
||||
|
||||
// Vitals tracks a collection of resource health.
|
||||
type Vitals []Vital
|
||||
|
|
@ -25,4 +25,6 @@ const (
|
|||
KeyApp ContextKey = "app"
|
||||
KeyStyles ContextKey = "styles"
|
||||
KeyMetrics ContextKey = "metrics"
|
||||
KeyToast ContextKey = "toast"
|
||||
KeyWithMetrics ContextKey = "withMetrics"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type (
|
|||
func NewCluster(f dao.Factory) *Cluster {
|
||||
return &Cluster{
|
||||
factory: f,
|
||||
mx: client.NewMetricsServer(f.Client()),
|
||||
mx: client.DialMetrics(f.Client()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultFlashDelay sets the flash clear delay.
|
||||
DefaultFlashDelay = 3 * time.Second
|
||||
|
||||
// FlashInfo represents an info message.
|
||||
FlashInfo FlashLevel = iota
|
||||
// FlashWarn represents an warning message.
|
||||
FlashWarn
|
||||
// FlashErr represents an error message.
|
||||
FlashErr
|
||||
)
|
||||
|
||||
type LevelMessage struct {
|
||||
Level FlashLevel
|
||||
Text string
|
||||
}
|
||||
|
||||
func newClearMessage() LevelMessage {
|
||||
return LevelMessage{}
|
||||
}
|
||||
|
||||
func (l LevelMessage) IsClear() bool {
|
||||
return l.Text == ""
|
||||
}
|
||||
|
||||
// FlashLevel represents flash message severity.
|
||||
type FlashLevel int
|
||||
|
||||
// FlashChan represents a flash event channel.
|
||||
type FlashChan chan LevelMessage
|
||||
|
||||
// FlashListener represents a text model listener.
|
||||
type FlashListener interface {
|
||||
// FlashChanged notifies the model changed.
|
||||
FlashChanged(FlashLevel, string)
|
||||
|
||||
// FlashCleared notifies when the filter changed.
|
||||
FlashCleared()
|
||||
}
|
||||
|
||||
// Flash represents a flash message model.
|
||||
type Flash struct {
|
||||
msg LevelMessage
|
||||
cancel context.CancelFunc
|
||||
delay time.Duration
|
||||
msgChan chan LevelMessage
|
||||
}
|
||||
|
||||
func NewFlash(dur time.Duration) *Flash {
|
||||
return &Flash{
|
||||
delay: dur,
|
||||
msgChan: make(FlashChan, 3),
|
||||
}
|
||||
}
|
||||
|
||||
// Channel returns the flash channel.
|
||||
func (f *Flash) Channel() FlashChan {
|
||||
return f.msgChan
|
||||
}
|
||||
|
||||
// Info displays an info flash message.
|
||||
func (f *Flash) Info(msg string) {
|
||||
f.SetMessage(FlashInfo, msg)
|
||||
}
|
||||
|
||||
// Infof displays a formatted info flash message.
|
||||
func (f *Flash) Infof(fmat string, args ...interface{}) {
|
||||
f.Info(fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// Warn displays a warning flash message.
|
||||
func (f *Flash) Warn(msg string) {
|
||||
log.Warn().Msg(msg)
|
||||
f.SetMessage(FlashWarn, msg)
|
||||
}
|
||||
|
||||
// Warnf displays a formatted warning flash message.
|
||||
func (f *Flash) Warnf(fmat string, args ...interface{}) {
|
||||
f.Warn(fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// Err displays an error flash message.
|
||||
func (f *Flash) Err(err error) {
|
||||
log.Error().Msg(err.Error())
|
||||
f.SetMessage(FlashErr, err.Error())
|
||||
}
|
||||
|
||||
// Errf displays a formatted error flash message.
|
||||
func (f *Flash) Errf(fmat string, args ...interface{}) {
|
||||
var err error
|
||||
for _, a := range args {
|
||||
switch e := a.(type) {
|
||||
case error:
|
||||
err = e
|
||||
}
|
||||
}
|
||||
log.Error().Err(err).Msgf(fmat, args...)
|
||||
f.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// Clear clears the flash message.
|
||||
func (f *Flash) Clear() {
|
||||
f.fireCleared()
|
||||
}
|
||||
|
||||
// SetMessage sets the flash level message.
|
||||
func (f *Flash) SetMessage(level FlashLevel, msg string) {
|
||||
if f.cancel != nil {
|
||||
f.cancel()
|
||||
f.cancel = nil
|
||||
}
|
||||
|
||||
f.setLevelMessage(LevelMessage{Level: level, Text: msg})
|
||||
f.fireFlashChanged()
|
||||
|
||||
var ctx context.Context
|
||||
ctx, f.cancel = context.WithCancel(context.Background())
|
||||
go f.refresh(ctx)
|
||||
}
|
||||
|
||||
func (f *Flash) refresh(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(f.delay):
|
||||
f.fireCleared()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flash) setLevelMessage(msg LevelMessage) {
|
||||
f.msg = msg
|
||||
}
|
||||
|
||||
func (f *Flash) fireFlashChanged() {
|
||||
f.msgChan <- f.msg
|
||||
}
|
||||
|
||||
func (f *Flash) fireCleared() {
|
||||
f.msgChan <- newClearMessage()
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFlash(t *testing.T) {
|
||||
const delay = 1 * time.Millisecond
|
||||
|
||||
uu := map[string]struct {
|
||||
level model.FlashLevel
|
||||
e string
|
||||
}{
|
||||
"info": {level: model.FlashInfo, e: "blee"},
|
||||
"warn": {level: model.FlashWarn, e: "blee"},
|
||||
"err": {level: model.FlashErr, e: "blee"},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
|
||||
t.Run(k, func(t *testing.T) {
|
||||
f := model.NewFlash(delay)
|
||||
v := newFlash()
|
||||
go v.listen(f.Channel())
|
||||
|
||||
switch u.level {
|
||||
case model.FlashInfo:
|
||||
f.Info(u.e)
|
||||
case model.FlashWarn:
|
||||
f.Warn(u.e)
|
||||
case model.FlashErr:
|
||||
f.Err(errors.New(u.e))
|
||||
}
|
||||
|
||||
time.Sleep(2 * delay)
|
||||
s, c, l, m := v.getMetrics()
|
||||
assert.Equal(t, 1, s)
|
||||
assert.Equal(t, u.level, l)
|
||||
assert.Equal(t, u.e, m)
|
||||
assert.Equal(t, 1, c)
|
||||
|
||||
close(f.Channel())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlashBurst(t *testing.T) {
|
||||
const delay = 1 * time.Millisecond
|
||||
|
||||
f := model.NewFlash(delay)
|
||||
v := newFlash()
|
||||
go v.listen(f.Channel())
|
||||
|
||||
count := 5
|
||||
for i := 1; i <= count; i++ {
|
||||
f.Info(fmt.Sprintf("test-%d", i))
|
||||
}
|
||||
|
||||
time.Sleep(2 * delay)
|
||||
s, c, l, m := v.getMetrics()
|
||||
assert.Equal(t, count, s)
|
||||
assert.Equal(t, model.FlashInfo, l)
|
||||
assert.Equal(t, fmt.Sprintf("test-%d", count), m)
|
||||
assert.Equal(t, 1, c)
|
||||
}
|
||||
|
||||
type flash struct {
|
||||
set, clear int
|
||||
level model.FlashLevel
|
||||
msg string
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
func newFlash() *flash {
|
||||
return &flash{}
|
||||
}
|
||||
|
||||
func (f *flash) getMetrics() (int, int, model.FlashLevel, string) {
|
||||
f.mx.RLock()
|
||||
defer f.mx.RUnlock()
|
||||
return f.set, f.clear, f.level, f.msg
|
||||
}
|
||||
|
||||
func (f *flash) listen(c model.FlashChan) {
|
||||
for m := range c {
|
||||
f.mx.Lock()
|
||||
{
|
||||
if m.IsClear() {
|
||||
f.clear++
|
||||
} else {
|
||||
f.set++
|
||||
f.level, f.msg = m.Level, m.Text
|
||||
}
|
||||
}
|
||||
f.mx.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/health"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
factory dao.Factory
|
||||
}
|
||||
|
||||
func NewHealth(f dao.Factory) *Health {
|
||||
return &Health{
|
||||
factory: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Health) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf("HealthCheck %v", time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
gvrs := []string{
|
||||
"v1/pods",
|
||||
"v1/events",
|
||||
"apps/v1/replicasets",
|
||||
"apps/v1/deployments",
|
||||
"apps/v1/statefulsets",
|
||||
"apps/v1/daemonsets",
|
||||
"batch/v1/jobs",
|
||||
"v1/persistentvolumes",
|
||||
}
|
||||
|
||||
hh := make([]runtime.Object, 0, 10)
|
||||
for _, gvr := range gvrs {
|
||||
c, err := h.check(ctx, ns, gvr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hh = append(hh, c)
|
||||
}
|
||||
|
||||
mm, err := h.checkMetrics()
|
||||
if err != nil {
|
||||
return hh, nil
|
||||
}
|
||||
for _, m := range mm {
|
||||
hh = append(hh, m)
|
||||
}
|
||||
|
||||
return hh, nil
|
||||
}
|
||||
|
||||
func (h *Health) checkMetrics() (health.Checks, error) {
|
||||
dial := client.DialMetrics(h.factory.Client())
|
||||
nmx, err := dial.FetchNodesMetrics()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Fetching metrics")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cpu, mem float64
|
||||
for _, mx := range nmx.Items {
|
||||
cpu += float64(mx.Usage.Cpu().MilliValue())
|
||||
mem += client.ToMB(mx.Usage.Memory().Value())
|
||||
}
|
||||
c1 := health.NewCheck("cpu")
|
||||
c1.Set(health.OK, int(math.Round(cpu)))
|
||||
c2 := health.NewCheck("mem")
|
||||
c2.Set(health.OK, int(math.Round(mem)))
|
||||
|
||||
return health.Checks{c1, c2}, nil
|
||||
}
|
||||
|
||||
func (h *Health) check(ctx context.Context, ns, gvr string) (*health.Check, error) {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf(" CHECK %s - %v", gvr, time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
meta, ok := Registry[gvr]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No meta for %q", gvr)
|
||||
}
|
||||
if meta.DAO == nil {
|
||||
meta.DAO = &dao.Resource{}
|
||||
}
|
||||
|
||||
meta.DAO.Init(h.factory, client.NewGVR(gvr))
|
||||
oo, err := meta.DAO.List(ctx, ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := health.NewCheck(gvr)
|
||||
c.Total(len(oo))
|
||||
rr, re := make(render.Rows, len(oo)), meta.Renderer
|
||||
for i, o := range oo {
|
||||
if err := re.Render(o, ns, &rr[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !render.Happy(ns, rr[i]) {
|
||||
c.Inc(health.Toast)
|
||||
} else {
|
||||
c.Inc(health.OK)
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/derailed/k9s/internal/health"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// PulseListener represents a health model listener.
|
||||
type PulseListener interface {
|
||||
// PulseChanged notifies the model data changed.
|
||||
PulseChanged(*health.Check)
|
||||
|
||||
// TreeFailed notifies the health check failed.
|
||||
PulseFailed(error)
|
||||
}
|
||||
|
||||
// Pulse tracks multiple resources health.
|
||||
type Pulse struct {
|
||||
gvr string
|
||||
namespace string
|
||||
inUpdate int32
|
||||
listeners []PulseListener
|
||||
refreshRate time.Duration
|
||||
health *Health
|
||||
data health.Checks
|
||||
}
|
||||
|
||||
func NewPulse(gvr string) *Pulse {
|
||||
return &Pulse{
|
||||
gvr: gvr,
|
||||
refreshRate: 2 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) Watch(ctx context.Context) {
|
||||
p.Refresh(ctx)
|
||||
go p.updater(ctx)
|
||||
}
|
||||
|
||||
func (p *Pulse) updater(ctx context.Context) {
|
||||
defer log.Debug().Msgf("Pulse canceled -- %q", p.gvr)
|
||||
|
||||
rate := initTreeRefreshRate
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(rate):
|
||||
rate = p.refreshRate
|
||||
p.refresh(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh update the model now.
|
||||
func (p *Pulse) Refresh(ctx context.Context) {
|
||||
for _, d := range p.data {
|
||||
p.firePulseChanged(d)
|
||||
}
|
||||
p.refresh(ctx)
|
||||
}
|
||||
|
||||
func (p *Pulse) refresh(ctx context.Context) {
|
||||
if !atomic.CompareAndSwapInt32(&p.inUpdate, 0, 1) {
|
||||
log.Debug().Msgf("Dropping update...")
|
||||
return
|
||||
}
|
||||
defer atomic.StoreInt32(&p.inUpdate, 0)
|
||||
|
||||
if err := p.reconcile(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("Reconcile failed")
|
||||
p.firePulseFailed(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) list(ctx context.Context) ([]runtime.Object, error) {
|
||||
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
|
||||
}
|
||||
if p.health == nil {
|
||||
p.health = NewHealth(f)
|
||||
}
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
return p.health.List(ctx, p.namespace)
|
||||
}
|
||||
|
||||
func (p *Pulse) reconcile(ctx context.Context) error {
|
||||
oo, err := p.list(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.data = health.Checks{}
|
||||
for _, o := range oo {
|
||||
c, ok := o.(*health.Check)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expecting health check but got %T", o)
|
||||
}
|
||||
p.data = append(p.data, c)
|
||||
p.firePulseChanged(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNamespace returns the model namespace.
|
||||
func (p *Pulse) GetNamespace() string {
|
||||
return p.namespace
|
||||
}
|
||||
|
||||
// SetNamespace sets up model namespace.
|
||||
func (p *Pulse) SetNamespace(ns string) {
|
||||
p.namespace = ns
|
||||
}
|
||||
|
||||
// AddListener adds a listener.
|
||||
func (p *Pulse) AddListener(l PulseListener) {
|
||||
p.listeners = append(p.listeners, l)
|
||||
}
|
||||
|
||||
// RemoveListener delete a listener.
|
||||
func (p *Pulse) RemoveListener(l PulseListener) {
|
||||
victim := -1
|
||||
for i, lis := range p.listeners {
|
||||
if lis == l {
|
||||
victim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if victim >= 0 {
|
||||
p.listeners = append(p.listeners[:victim], p.listeners[victim+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) firePulseChanged(check *health.Check) {
|
||||
for _, l := range p.listeners {
|
||||
l.PulseChanged(check)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pulse) firePulseFailed(err error) {
|
||||
for _, l := range p.listeners {
|
||||
l.PulseFailed(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,9 @@ var Registry = map[string]ResourceMeta{
|
|||
DAO: &dao.Chart{},
|
||||
Renderer: &render.Chart{},
|
||||
},
|
||||
"pulses": {
|
||||
DAO: &dao.Pulse{},
|
||||
},
|
||||
"openfaas": {
|
||||
DAO: &dao.OpenFaas{},
|
||||
Renderer: &render.OpenFaas{},
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ func (s *Stack) Pop() (Component, bool) {
|
|||
var c Component
|
||||
s.mx.Lock()
|
||||
{
|
||||
c = s.components[s.size()]
|
||||
s.components = s.components[:s.size()]
|
||||
c = s.components[len(s.components)-1]
|
||||
s.components = s.components[:len(s.components)-1]
|
||||
}
|
||||
s.mx.Unlock()
|
||||
s.notify(StackPop, c)
|
||||
|
|
@ -163,11 +163,7 @@ func (s *Stack) Top() Component {
|
|||
return nil
|
||||
}
|
||||
|
||||
return s.components[s.size()]
|
||||
}
|
||||
|
||||
func (s *Stack) size() int {
|
||||
return len(s.components) - 1
|
||||
return s.components[len(s.components)-1]
|
||||
}
|
||||
|
||||
func (s *Stack) notify(a StackAction, c Component) {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ func (t *Table) SetRefreshRate(d time.Duration) {
|
|||
|
||||
// ClusterWide checks if resource is scope for all namespaces.
|
||||
func (t *Table) ClusterWide() bool {
|
||||
log.Debug().Msgf("CLUSTER-WIDE %q", t.namespace)
|
||||
return client.IsClusterWide(t.namespace)
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +220,7 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
|||
if client.IsClusterScoped(t.namespace) {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
|
||||
return a.List(ctx, ns)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ func TestTableReconcile(t *testing.T) {
|
|||
f.rows = []runtime.Object{load(t, "p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
err := ta.reconcile(ctx)
|
||||
assert.Nil(t, err)
|
||||
data := ta.Peek()
|
||||
assert.Equal(t, 15, len(data.Header))
|
||||
assert.Equal(t, 17, len(data.Header))
|
||||
assert.Equal(t, 1, len(data.RowEvents))
|
||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||
}
|
||||
|
|
@ -55,6 +56,7 @@ func TestTableGet(t *testing.T) {
|
|||
f := makeFactory()
|
||||
f.rows = []runtime.Object{load(t, "p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
row, err := ta.Get(ctx, "fred")
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, row)
|
||||
|
|
@ -104,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
|
|||
|
||||
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
||||
assert.Equal(t, 1, len(rr))
|
||||
assert.Equal(t, 14, len(rr[0].Fields))
|
||||
assert.Equal(t, 16, len(rr[0].Fields))
|
||||
}
|
||||
|
||||
func TestTableGenericHydrate(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,10 @@ func TestTableRefresh(t *testing.T) {
|
|||
f.rows = []runtime.Object{mustLoad("p1")}
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||
ta.Refresh(ctx)
|
||||
data := ta.Peek()
|
||||
assert.Equal(t, 15, len(data.Header))
|
||||
assert.Equal(t, 17, len(data.Header))
|
||||
assert.Equal(t, 1, len(data.RowEvents))
|
||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||
assert.Equal(t, 1, l.count)
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
|||
}
|
||||
if t.root == nil || t.root.Diff(root) {
|
||||
t.root = root
|
||||
t.fireTreeTreeChanged(t.root)
|
||||
t.fireTreeChanged(t.root)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -251,7 +251,7 @@ func (t *Tree) resourceMeta() ResourceMeta {
|
|||
return meta
|
||||
}
|
||||
|
||||
func (t *Tree) fireTreeTreeChanged(root *xray.TreeNode) {
|
||||
func (t *Tree) fireTreeChanged(root *xray.TreeNode) {
|
||||
for _, l := range t.listeners {
|
||||
l.TreeChanged(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
|
@ -8,6 +9,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"golang.org/x/text/language"
|
||||
|
|
@ -28,11 +30,10 @@ var (
|
|||
type Benchmark struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Benchmark) ColorerFunc() ColorerFunc {
|
||||
func (b Benchmark) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := tcell.ColorPaleGreen
|
||||
statusCol := 2
|
||||
if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" {
|
||||
if !Happy(ns, re.Row) {
|
||||
c = ErrColor
|
||||
}
|
||||
return c
|
||||
|
|
@ -50,6 +51,7 @@ func (Benchmark) Header(ns string) HeaderRow {
|
|||
Header{Name: "2XX", Align: tview.AlignRight},
|
||||
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||
Header{Name: "REPORT"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +74,24 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
|
|||
return err
|
||||
}
|
||||
b.augmentRow(r.Fields, data)
|
||||
r.Fields[8] = asStatus(b.diagnose(ns, r.Fields))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (Benchmark) diagnose(ns string, ff Fields) error {
|
||||
statusCol := 3
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
statusCol--
|
||||
}
|
||||
|
||||
if len(ff) < statusCol {
|
||||
return nil
|
||||
}
|
||||
if ff[statusCol] != "pass" {
|
||||
return errors.New("failed benchmark")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -87,7 +107,7 @@ func (Benchmark) readFile(file string) (string, error) {
|
|||
return string(data), nil
|
||||
}
|
||||
|
||||
func (Benchmark) initRow(row Fields, f os.FileInfo) error {
|
||||
func (b Benchmark) initRow(row Fields, f os.FileInfo) error {
|
||||
tokens := strings.Split(f.Name(), "_")
|
||||
if len(tokens) < 2 {
|
||||
return fmt.Errorf("Invalid file name %s", f.Name())
|
||||
|
|
@ -95,7 +115,7 @@ func (Benchmark) initRow(row Fields, f os.FileInfo) error {
|
|||
row[0] = tokens[0]
|
||||
row[1] = tokens[1]
|
||||
row[7] = f.Name()
|
||||
row[8] = timeToAge(f.ModTime())
|
||||
row[9] = timeToAge(f.ModTime())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ type Chart struct{}
|
|||
// ColorerFunc colors a resource row.
|
||||
func (Chart) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return tcell.ColorMediumSpringGreen
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +39,7 @@ func (Chart) Header(ns string) HeaderRow {
|
|||
Header{Name: "STATUS"},
|
||||
Header{Name: "CHART"},
|
||||
Header{Name: "APP VERSION"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -57,12 +62,21 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
|
|||
h.Release.Info.Status.String(),
|
||||
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
||||
h.Release.Chart.Metadata.AppVersion,
|
||||
asStatus(c.diagnose(h.Release.Info.Status.String())),
|
||||
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Chart) diagnose(s string) error {
|
||||
if s != "deployed" {
|
||||
return fmt.Errorf("chart is in an invalid state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -36,18 +37,18 @@ type ContainerWithMetrics interface {
|
|||
// Container renders a K8s Container to screen.
|
||||
type Container struct{}
|
||||
|
||||
const readyCol = 2
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Container) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
func (c Container) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
color := DefaultColorer(ns, re)
|
||||
|
||||
readyCol := 2
|
||||
if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" {
|
||||
c = ErrColor
|
||||
if !Happy(ns, re.Row) {
|
||||
color = ErrColor
|
||||
}
|
||||
|
||||
stateCol := readyCol + 1
|
||||
switch strings.TrimSpace(r.Row.Fields[stateCol]) {
|
||||
switch strings.TrimSpace(re.Row.Fields[stateCol]) {
|
||||
case ContainerCreating, PodInitializing:
|
||||
return AddColor
|
||||
case Terminating, Initialized:
|
||||
|
|
@ -56,10 +57,10 @@ func (Container) ColorerFunc() ColorerFunc {
|
|||
return CompletedColor
|
||||
case Running:
|
||||
default:
|
||||
c = ErrColor
|
||||
color = ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ func (Container) Header(ns string) HeaderRow {
|
|||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -114,12 +116,25 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
|||
limit.cpu,
|
||||
limit.mem,
|
||||
toStrPorts(co.Container.Ports),
|
||||
asStatus(c.diagnose(state, ready)),
|
||||
toAge(co.Age),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (Container) diagnose(state, ready string) error {
|
||||
if state == "Completed" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ready == "false" {
|
||||
return errors.New("container is not ready")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func TestContainer(t *testing.T) {
|
|||
"50",
|
||||
"20",
|
||||
"",
|
||||
"container is not ready",
|
||||
},
|
||||
r.Fields[:len(r.Fields)-1],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
|
|||
func (ClusterRole) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +41,7 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
|
|||
r.ID = client.FQN("-", cr.ObjectMeta.Name)
|
||||
r.Fields = Fields{
|
||||
cr.Name,
|
||||
mapToStr(cr.Labels),
|
||||
toAge(cr.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ func (ClusterRoleBinding) Header(string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "CLUSTERROLE"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "SUBJECT-KIND"},
|
||||
Header{Name: "SUBJECTS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +49,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
|
|||
crb.RoleRef.Name,
|
||||
kind,
|
||||
ss,
|
||||
mapToStr(crb.Labels),
|
||||
toAge(crb.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestClusterRoleBindingRender(t *testing.T) {
|
|||
c.Render(load(t, "crb"), "-", &r)
|
||||
|
||||
assert.Equal(t, "-/blee", r.ID)
|
||||
assert.Equal(t, render.Fields{"blee", "blee", "USR", "fernand"}, r.Fields[:4])
|
||||
assert.Equal(t, render.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc {
|
|||
func (CustomResourceDefinition) Header(string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +46,7 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error {
|
|||
r.ID = client.FQN(client.ClusterScope, extractMetaField(meta, "name"))
|
||||
r.Fields = Fields{
|
||||
extractMetaField(meta, "name"),
|
||||
mapToIfc(meta["labels"]),
|
||||
toAge(metav1.Time{Time: t}),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
|
@ -31,6 +34,11 @@ func (CronJob) Header(ns string) HeaderRow {
|
|||
Header{Name: "SUSPEND"},
|
||||
Header{Name: "ACTIVE"},
|
||||
Header{Name: "LAST_SCHEDULE"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -63,8 +71,64 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
|
|||
boolPtrToStr(cj.Spec.Suspend),
|
||||
strconv.Itoa(len(cj.Status.Active)),
|
||||
lastScheduled,
|
||||
jobSelector(cj.Spec.JobTemplate.Spec),
|
||||
podContainerNames(cj.Spec.JobTemplate.Spec.Template.Spec, true),
|
||||
podImageNames(cj.Spec.JobTemplate.Spec.Template.Spec, true),
|
||||
mapToStr(cj.Labels),
|
||||
"",
|
||||
toAge(cj.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func jobSelector(spec batchv1.JobSpec) string {
|
||||
if spec.Selector == nil {
|
||||
return MissingValue
|
||||
}
|
||||
if len(spec.Selector.MatchLabels) > 0 {
|
||||
return mapToStr(spec.Selector.MatchLabels)
|
||||
}
|
||||
if len(spec.Selector.MatchExpressions) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ss := make([]string, 0, len(spec.Selector.MatchExpressions))
|
||||
for _, e := range spec.Selector.MatchExpressions {
|
||||
ss = append(ss, e.String())
|
||||
}
|
||||
|
||||
return strings.Join(ss, " ")
|
||||
}
|
||||
|
||||
func podContainerNames(spec v1.PodSpec, includeInit bool) string {
|
||||
cc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))
|
||||
|
||||
if includeInit {
|
||||
for _, c := range spec.InitContainers {
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
}
|
||||
for _, c := range spec.Containers {
|
||||
cc = append(cc, c.Name)
|
||||
}
|
||||
|
||||
return strings.Join(cc, ",")
|
||||
}
|
||||
|
||||
func podImageNames(spec v1.PodSpec, includeInit bool) string {
|
||||
cc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))
|
||||
|
||||
if includeInit {
|
||||
for _, c := range spec.InitContainers {
|
||||
cc = append(cc, c.Image)
|
||||
}
|
||||
}
|
||||
for _, c := range spec.Containers {
|
||||
cc = append(cc, c.Image)
|
||||
}
|
||||
|
||||
return strings.Join(cc, ",")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -17,19 +16,13 @@ import (
|
|||
type Deployment struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Deployment) ColorerFunc() ColorerFunc {
|
||||
func (d Deployment) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
readyCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
readyCol--
|
||||
}
|
||||
tokens := strings.Split(r.Row.Fields[readyCol], "/")
|
||||
if tokens[0] != tokens[1] {
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +42,9 @@ func (Deployment) Header(ns string) HeaderRow {
|
|||
Header{Name: "READY"},
|
||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -73,11 +69,21 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
r.Fields = append(r.Fields,
|
||||
dp.Name,
|
||||
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(*dp.Spec.Replicas)),
|
||||
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(dp.Status.Replicas)),
|
||||
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
|
||||
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
||||
strconv.Itoa(int(dp.Status.ReadyReplicas)),
|
||||
mapToStr(dp.Labels),
|
||||
asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
|
||||
toAge(dp.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Deployment) diagnose(d, r int32) error {
|
||||
if d != r {
|
||||
return fmt.Errorf("desiring %d replicas got %d available", d, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -17,18 +16,14 @@ import (
|
|||
type DaemonSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (DaemonSet) ColorerFunc() ColorerFunc {
|
||||
func (d DaemonSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
desiredCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
desiredCol = 1
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[desiredCol]) != strings.TrimSpace(r.Row.Fields[desiredCol+2]) {
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +45,8 @@ func (DaemonSet) Header(ns string) HeaderRow {
|
|||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -78,8 +75,18 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
|
|||
strconv.Itoa(int(ds.Status.NumberReady)),
|
||||
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
|
||||
strconv.Itoa(int(ds.Status.NumberAvailable)),
|
||||
mapToStr(ds.Labels),
|
||||
asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),
|
||||
toAge(ds.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (DaemonSet) diagnose(d, r int32) error {
|
||||
if d != r {
|
||||
return fmt.Errorf("desiring %d replicas but %d ready", d, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -17,19 +18,20 @@ import (
|
|||
type Event struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Event) ColorerFunc() ColorerFunc {
|
||||
func (e Event) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
markCol := 3
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol = 2
|
||||
}
|
||||
switch strings.TrimSpace(r.Row.Fields[markCol]) {
|
||||
case "Failed":
|
||||
c = ErrColor
|
||||
case "Killing":
|
||||
c = KillColor
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) == "Killing" {
|
||||
return KillColor
|
||||
}
|
||||
|
||||
return c
|
||||
|
|
@ -45,10 +47,12 @@ func (Event) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "TYPE"},
|
||||
Header{Name: "REASON"},
|
||||
Header{Name: "SOURCE"},
|
||||
Header{Name: "COUNT", Align: tview.AlignRight},
|
||||
Header{Name: "MESSAGE"},
|
||||
Header{Name: "MESSAGE", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -72,15 +76,27 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
r.Fields = append(r.Fields,
|
||||
asRef(ev.InvolvedObject),
|
||||
ev.Type,
|
||||
ev.Reason,
|
||||
ev.Source.Component,
|
||||
strconv.Itoa(int(ev.Count)),
|
||||
ev.Message,
|
||||
asStatus(e.diagnose(ev.Type)),
|
||||
toAge(ev.LastTimestamp))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (Event) diagnose(kind string) error {
|
||||
if kind != "Normal" {
|
||||
return errors.New("failed event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func asRef(r v1.ObjectReference) string {
|
||||
return strings.ToLower(r.Kind) + ":" + r.Name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,17 @@ func TestEventRender(t *testing.T) {
|
|||
c.Render(load(t, "ev"), "", &r)
|
||||
|
||||
assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
|
||||
assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:6])
|
||||
assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7])
|
||||
}
|
||||
|
||||
func BenchmarkEventRender(b *testing.B) {
|
||||
ev := load(b, "ev")
|
||||
var re render.Event
|
||||
r := render.NewRow(7)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = re.Render(&ev, "", &r)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ type Generic struct {
|
|||
ageIndex int
|
||||
}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (Generic) Happy(ns string, r Row) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetTable sets the tabular resource.
|
||||
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
||||
g.table = t
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
)
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func Happy(ns string, r Row) bool {
|
||||
validCol := r.Len() - 2
|
||||
return strings.TrimSpace(r.Fields[validCol]) == ""
|
||||
}
|
||||
|
||||
const megaByte = 1024 * 1024
|
||||
|
||||
// ToMB converts bytes to megabytes.
|
||||
|
|
@ -20,6 +26,13 @@ func ToMB(v int64) float64 {
|
|||
return float64(v) / megaByte
|
||||
}
|
||||
|
||||
func asStatus(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func asSelector(s *metav1.LabelSelector) string {
|
||||
sel, err := metav1.LabelSelectorAsSelector(s)
|
||||
if err != nil {
|
||||
|
|
@ -84,7 +97,7 @@ func join(a []string, sep string) string {
|
|||
|
||||
var buff strings.Builder
|
||||
buff.Grow(n)
|
||||
buff.WriteString(a[0])
|
||||
buff.WriteString(b[0])
|
||||
for _, s := range b[1:] {
|
||||
buff.WriteString(sep)
|
||||
buff.WriteString(s)
|
||||
|
|
@ -163,7 +176,40 @@ func mapToStr(m map[string]string) (s string) {
|
|||
for i, k := range kk {
|
||||
s += k + "=" + m[k]
|
||||
if i < len(kk)-1 {
|
||||
s += ","
|
||||
s += " "
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func mapToIfc(m interface{}) (s string) {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
mm, ok := m.(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(mm) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
kk := make([]string, 0, len(mm))
|
||||
for k := range mm {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Strings(kk)
|
||||
|
||||
for i, k := range kk {
|
||||
str, ok := mm[k].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
s += k + "=" + str
|
||||
if i < len(kk)-1 {
|
||||
s += " "
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,10 +82,11 @@ func TestJoin(t *testing.T) {
|
|||
i []string
|
||||
e string
|
||||
}{
|
||||
"zero": {[]string{}, ""},
|
||||
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
||||
"blank": {[]string{"", "", ""}, ""},
|
||||
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
||||
"zero": {[]string{}, ""},
|
||||
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
||||
"blank": {[]string{"", "", ""}, ""},
|
||||
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
||||
"withBlank": {[]string{"", "a", "c"}, "a,c"},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
|
|
@ -304,7 +305,7 @@ func TestMapToStr(t *testing.T) {
|
|||
i map[string]string
|
||||
e string
|
||||
}{
|
||||
{map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"},
|
||||
{map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb blee=duh"},
|
||||
{map[string]string{}, ""},
|
||||
}
|
||||
for _, u := range uu {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
|
|||
Header{Name: "MINPODS", Align: tview.AlignRight},
|
||||
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -80,6 +81,7 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
|
|||
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
@ -106,6 +108,7 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
|
|||
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
@ -132,6 +135,7 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
|
|||
strconv.Itoa(int(*hpa.Spec.MinReplicas)),
|
||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||
"",
|
||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func (Ingress) Header(ns string) HeaderRow {
|
|||
Header{Name: "HOSTS"},
|
||||
Header{Name: "ADDRESS"},
|
||||
Header{Name: "PORT"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -57,6 +58,7 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
|
|||
toHosts(ing.Spec.Rules),
|
||||
toAddress(ing.Status.LoadBalancer),
|
||||
toTLSPorts(ing.Spec.TLS),
|
||||
"",
|
||||
toAge(ing.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/derailed/k9s/internal/client"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
|
|
@ -33,8 +34,10 @@ func (Job) Header(ns string) HeaderRow {
|
|||
Header{Name: "NAME"},
|
||||
Header{Name: "COMPLETIONS"},
|
||||
Header{Name: "DURATION"},
|
||||
Header{Name: "CONTAINERS"},
|
||||
Header{Name: "IMAGES"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -50,6 +53,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ready := toCompletion(job.Spec, job.Status)
|
||||
|
||||
r.ID = client.MetaFQN(job.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(j.Header(ns)))
|
||||
|
|
@ -59,16 +63,29 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
|||
cc, ii := toContainers(job.Spec.Template.Spec)
|
||||
r.Fields = append(r.Fields,
|
||||
job.Name,
|
||||
toCompletion(job.Spec, job.Status),
|
||||
ready,
|
||||
toDuration(job.Status),
|
||||
jobSelector(job.Spec),
|
||||
cc,
|
||||
ii,
|
||||
asStatus(j.diagnose(ready, job.Status.CompletionTime)),
|
||||
toAge(job.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Job) diagnose(ready string, completed *metav1.Time) error {
|
||||
if completed == nil {
|
||||
return nil
|
||||
}
|
||||
tokens := strings.Split(ready, "/")
|
||||
if tokens[0] != tokens[1] {
|
||||
return fmt.Errorf("expecting %s completion got %s", tokens[1], tokens[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestJobRender(t *testing.T) {
|
|||
c.Render(load(t, "job"), "", &r)
|
||||
|
||||
assert.Equal(t, "default/hello-1567179180", r.ID)
|
||||
assert.Equal(t, render.Fields{"default", "hello-1567179180", "1/1", "8s", "c1", "blang/busybox-bash"}, r.Fields[:6])
|
||||
assert.Equal(t, render.Fields{"default", "hello-1567179180", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:7])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -22,8 +25,16 @@ const (
|
|||
type Node struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Node) ColorerFunc() ColorerFunc {
|
||||
return DefaultColorer
|
||||
func (n Node) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
|
|
@ -31,17 +42,19 @@ func (Node) Header(_ string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "ROLE"},
|
||||
Header{Name: "VERSION"},
|
||||
Header{Name: "KERNEL"},
|
||||
Header{Name: "INTERNAL-IP"},
|
||||
Header{Name: "EXTERNAL-IP"},
|
||||
Header{Name: "ROLE", Wide: true},
|
||||
Header{Name: "VERSION", Wide: true},
|
||||
Header{Name: "KERNEL", Wide: true},
|
||||
Header{Name: "INTERNAL-IP", Wide: true},
|
||||
Header{Name: "EXTERNAL-IP", Wide: true},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||
Header{Name: "ACPU", Align: tview.AlignRight},
|
||||
Header{Name: "AMEM", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -69,17 +82,19 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
|||
|
||||
c, a, p := gatherNodeMX(&no, oo.MX)
|
||||
|
||||
sta := make([]string, 10)
|
||||
status(no.Status, no.Spec.Unschedulable, sta)
|
||||
ro := make([]string, 10)
|
||||
nodeRoles(&no, ro)
|
||||
statuses := make(sort.StringSlice, 10)
|
||||
status(no.Status, no.Spec.Unschedulable, statuses)
|
||||
sort.Sort(statuses)
|
||||
roles := make(sort.StringSlice, 10)
|
||||
nodeRoles(&no, roles)
|
||||
sort.Sort(roles)
|
||||
|
||||
r.ID = client.FQN("", na)
|
||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||
r.Fields = append(r.Fields,
|
||||
no.Name,
|
||||
join(sta, ","),
|
||||
join(ro, ","),
|
||||
join(statuses, ","),
|
||||
join(roles, ","),
|
||||
no.Status.NodeInfo.KubeletVersion,
|
||||
no.Status.NodeInfo.KernelVersion,
|
||||
iIP,
|
||||
|
|
@ -90,12 +105,27 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
|||
p.mem,
|
||||
a.cpu,
|
||||
a.mem,
|
||||
mapToStr(no.Labels),
|
||||
asStatus(n.diagnose(statuses)),
|
||||
toAge(no.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Node) diagnose(ss []string) error {
|
||||
if len(ss) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, s := range ss {
|
||||
if s == "Ready" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("node is not ready")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
@ -156,6 +186,9 @@ func nodeRoles(node *v1.Node, res []string) {
|
|||
res[index] = v
|
||||
index++
|
||||
}
|
||||
if index >= len(res) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty(res) {
|
||||
|
|
|
|||
|
|
@ -28,12 +28,14 @@ func (NetworkPolicy) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "ING-SELECTOR"},
|
||||
Header{Name: "ING-SELECTOR", Wide: true},
|
||||
Header{Name: "ING-PORTS"},
|
||||
Header{Name: "ING-BLOCK"},
|
||||
Header{Name: "EGR-SELECTOR"},
|
||||
Header{Name: "EGR-SELECTOR", Wide: true},
|
||||
Header{Name: "EGR-PORTS"},
|
||||
Header{Name: "EGR-BLOCK"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -66,6 +68,8 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
|
|||
es,
|
||||
ep,
|
||||
eb,
|
||||
mapToStr(np.Labels),
|
||||
"",
|
||||
toAge(np.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ import (
|
|||
type Namespace struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Namespace) ColorerFunc() ColorerFunc {
|
||||
func (n Namespace) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd {
|
||||
|
|
@ -25,9 +26,8 @@ func (Namespace) ColorerFunc() ColorerFunc {
|
|||
if r.Kind == EventUpdate {
|
||||
c = StdColor
|
||||
}
|
||||
switch strings.TrimSpace(r.Row.Fields[1]) {
|
||||
case "Inactive", Terminating:
|
||||
c = ErrColor
|
||||
if !Happy(ns, r.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||
c = HighlightColor
|
||||
|
|
@ -42,12 +42,14 @@ func (Namespace) Header(string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
func (Namespace) Render(o interface{}, _ string, r *Row) error {
|
||||
func (n Namespace) Render(o interface{}, _ string, r *Row) error {
|
||||
raw, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return fmt.Errorf("Expected Namespace, but got %T", o)
|
||||
|
|
@ -62,8 +64,17 @@ func (Namespace) Render(o interface{}, _ string, r *Row) error {
|
|||
r.Fields = Fields{
|
||||
ns.Name,
|
||||
string(ns.Status.Phase),
|
||||
mapToStr(ns.Labels),
|
||||
asStatus(n.diagnose(ns.Status.Phase)),
|
||||
toAge(ns.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Namespace) diagnose(phase v1.NamespacePhase) error {
|
||||
if phase != v1.NamespaceActive && phase != v1.NamespaceTerminating {
|
||||
return errors.New("namespace not ready")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
|
@ -23,8 +24,12 @@ const (
|
|||
type OpenFaas struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (OpenFaas) ColorerFunc() ColorerFunc {
|
||||
func (o OpenFaas) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return tcell.ColorPaleTurquoise
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +49,7 @@ func (OpenFaas) Header(ns string) HeaderRow {
|
|||
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -77,12 +83,21 @@ func (f OpenFaas) Render(o interface{}, ns string, r *Row) error {
|
|||
strconv.Itoa(int(fn.Function.InvocationCount)),
|
||||
strconv.Itoa(int(fn.Function.Replicas)),
|
||||
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
||||
asStatus(f.diagnose(status)),
|
||||
toAge(metav1.Time{Time: time.Now()}),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (OpenFaas) diagnose(status string) error {
|
||||
if status != "Ready" {
|
||||
return errors.New("function not ready")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -18,24 +17,19 @@ import (
|
|||
type PodDisruptionBudget struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (p PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
markCol := 5
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol--
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
|
|
@ -53,6 +47,8 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow {
|
|||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -82,12 +78,21 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
|
|||
strconv.Itoa(int(pdb.Status.CurrentHealthy)),
|
||||
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
||||
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
||||
mapToStr(pdb.Labels),
|
||||
asStatus(p.diagnose(pdb.Spec.MinAvailable.IntVal, pdb.Status.CurrentHealthy)),
|
||||
toAge(pdb.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PodDisruptionBudget) diagnose(min, healthy int32) error {
|
||||
if min > healthy {
|
||||
return fmt.Errorf("expected %d but got %d", min, healthy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
func numbToStr(n *intstr.IntOrString) string {
|
||||
|
|
|
|||
|
|
@ -25,15 +25,11 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
|||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
|
||||
readyCol := 2
|
||||
statusCol := 4
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
readyCol--
|
||||
statusCol--
|
||||
}
|
||||
statusCol := readyCol + 1
|
||||
|
||||
ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol])
|
||||
c = p.checkReadyCol(ready, status, c)
|
||||
|
||||
status := strings.TrimSpace(re.Row.Fields[statusCol])
|
||||
switch status {
|
||||
case ContainerCreating, PodInitializing:
|
||||
c = AddColor
|
||||
|
|
@ -42,28 +38,19 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
|||
case Completed:
|
||||
c = CompletedColor
|
||||
case Running:
|
||||
c = StdColor
|
||||
case Terminating:
|
||||
c = KillColor
|
||||
default:
|
||||
c = ErrColor
|
||||
if !Happy(ns, re.Row) {
|
||||
c = ErrColor
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
func (Pod) checkReadyCol(readyCol, statusCol string, c tcell.Color) tcell.Color {
|
||||
if statusCol == "Completed" {
|
||||
return c
|
||||
}
|
||||
|
||||
tokens := strings.Split(readyCol, "/")
|
||||
if len(tokens) == 2 && (tokens[0] == "0" || tokens[0] != tokens[1]) {
|
||||
return ErrColor
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
func (Pod) Header(ns string) HeaderRow {
|
||||
var h HeaderRow
|
||||
|
|
@ -74,17 +61,19 @@ func (Pod) Header(ns string) HeaderRow {
|
|||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "RS", Align: tview.AlignRight},
|
||||
Header{Name: "STATUS"},
|
||||
Header{Name: "CPU", Align: tview.AlignRight},
|
||||
Header{Name: "MEM", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||
Header{Name: "IP"},
|
||||
Header{Name: "NODE"},
|
||||
Header{Name: "QOS"},
|
||||
Header{Name: "IP", Wide: true},
|
||||
Header{Name: "NODE", Wide: true},
|
||||
Header{Name: "QOS", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -105,7 +94,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
ss := po.Status.ContainerStatuses
|
||||
cr, _, rc := p.Statuses(ss)
|
||||
c, perc := p.gatherPodMX(&po, pwm.MX)
|
||||
|
||||
phase := p.Phase(&po)
|
||||
r.ID = client.MetaFQN(po.ObjectMeta)
|
||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||
if client.IsAllNamespaces(ns) {
|
||||
|
|
@ -114,8 +103,8 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
r.Fields = append(r.Fields,
|
||||
po.ObjectMeta.Name,
|
||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||
p.Phase(&po),
|
||||
strconv.Itoa(rc),
|
||||
phase,
|
||||
c.cpu,
|
||||
c.mem,
|
||||
perc.cpu,
|
||||
|
|
@ -125,12 +114,25 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
na(po.Status.PodIP),
|
||||
na(po.Spec.NodeName),
|
||||
p.mapQOS(po.Status.QOSClass),
|
||||
mapToStr(po.Labels),
|
||||
asStatus(p.diagnose(phase, cr, len(ss))),
|
||||
toAge(po.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Pod) diagnose(phase string, cr, ct int) error {
|
||||
if phase == "Completed" {
|
||||
return nil
|
||||
}
|
||||
if cr != ct {
|
||||
return fmt.Errorf("container ready check failed: %d of %d", cr, ct)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ type (
|
|||
|
||||
func TestPodColorer(t *testing.T) {
|
||||
var (
|
||||
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Running"}}
|
||||
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Boom"}}
|
||||
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "Boom"}}
|
||||
row = render.Row{Fields: render.Fields{"fred", "1/1", "Running"}}
|
||||
toast = render.Row{Fields: render.Fields{"fred", "1/1", "Boom"}}
|
||||
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "Boom"}}
|
||||
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Running"}}
|
||||
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Boom"}}
|
||||
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "0", "Boom"}}
|
||||
row = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Running"}}
|
||||
toast = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Boom"}}
|
||||
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "0", "Boom"}}
|
||||
)
|
||||
|
||||
uu := colorerUCs{
|
||||
|
|
@ -70,7 +70,7 @@ func TestPodRender(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "default/nginx", r.ID)
|
||||
e := render.Fields{"default", "nginx", "1/1", "Running", "0", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
||||
e := render.Fields{"default", "nginx", "1/1", "0", "Running", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
||||
assert.Equal(t, e, r.Fields[:14])
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ func TestPodInitRender(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "default/nginx", r.ID)
|
||||
e := render.Fields{"default", "nginx", "1/1", "Init:0/1", "0", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
||||
e := render.Fields{"default", "nginx", "1/1", "0", "Init:0/1", "10", "10", "10", "14", "0", "5", "172.17.0.6", "minikube", "BE"}
|
||||
assert.Equal(t, e, r.Fields[:14])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ func rbacVerbHeader() HeaderRow {
|
|||
Header{Name: "PATCH "},
|
||||
Header{Name: "UPDATE"},
|
||||
Header{Name: "DELETE"},
|
||||
Header{Name: "DLIST "},
|
||||
Header{Name: "EXTRAS"},
|
||||
Header{Name: "DEL-LIST "},
|
||||
Header{Name: "EXTRAS", Wide: true},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,10 @@ func (Policy) Header(ns string) HeaderRow {
|
|||
Header{Name: "API GROUP"},
|
||||
Header{Name: "BINDING"},
|
||||
}
|
||||
return append(h, rbacVerbHeader()...)
|
||||
h = append(h, rbacVerbHeader()...)
|
||||
h = append(h, Header{Name: "VALID", Wide: true})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -52,8 +55,14 @@ func (Policy) Render(o interface{}, gvr string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = client.FQN(p.Namespace, p.Resource)
|
||||
r.Fields = append(r.Fields, p.Namespace, cleanseResource(p.Resource), p.Group, p.Binding)
|
||||
r.Fields = append(r.Fields,
|
||||
p.Namespace,
|
||||
cleanseResource(p.Resource),
|
||||
p.Group,
|
||||
p.Binding,
|
||||
)
|
||||
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
||||
r.Fields = append(r.Fields, "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,6 @@ func TestPolicyRender(t *testing.T) {
|
|||
"[orangered::b] 𐄂 [::]",
|
||||
"[orangered::b] 𐄂 [::]",
|
||||
"",
|
||||
"",
|
||||
}, r.Fields)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func TestPortForwardRender(t *testing.T) {
|
|||
"http://0.0.0.0:p1/",
|
||||
"1",
|
||||
"1",
|
||||
"",
|
||||
"2m",
|
||||
}, r.Fields)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ func (PortForward) Header(ns string) HeaderRow {
|
|||
Header{Name: "URL"},
|
||||
Header{Name: "C"},
|
||||
Header{Name: "N"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +72,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
|
|||
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
|
||||
asNum(pf.Config.C),
|
||||
asNum(pf.Config.N),
|
||||
"",
|
||||
pf.Age(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,26 +16,26 @@ import (
|
|||
type PersistentVolume struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PersistentVolume) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (p PersistentVolume) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(r.Row.Fields[4])
|
||||
switch status {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(re.Row.Fields[4]) {
|
||||
case "Bound":
|
||||
c = StdColor
|
||||
case "Available":
|
||||
c = tcell.ColorYellow
|
||||
default:
|
||||
c = ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header rbw.
|
||||
|
|
@ -49,6 +49,9 @@ func (PersistentVolume) Header(string) HeaderRow {
|
|||
Header{Name: "CLAIM"},
|
||||
Header{Name: "STORAGECLASS"},
|
||||
Header{Name: "REASON"},
|
||||
Header{Name: "VOLUMEMODE", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -90,12 +93,30 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
|
|||
claim,
|
||||
class,
|
||||
pv.Status.Reason,
|
||||
p.volumeMode(pv.Spec.VolumeMode),
|
||||
mapToStr(pv.Labels),
|
||||
asStatus(p.diagnose(string(phase))),
|
||||
toAge(pv.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PersistentVolume) diagnose(r string) error {
|
||||
if r != "Bound" && r != "Available" {
|
||||
return fmt.Errorf("unexpected status %s", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PersistentVolume) volumeMode(m *v1.PersistentVolumeMode) string {
|
||||
if m == nil {
|
||||
return MissingValue
|
||||
}
|
||||
|
||||
return string(*m)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package render
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -15,19 +14,14 @@ import (
|
|||
type PersistentVolumeClaim struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (p PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
markCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol--
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" {
|
||||
c = ErrColor
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return c
|
||||
|
|
@ -49,6 +43,8 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow {
|
|||
Header{Name: "CAPACITY"},
|
||||
Header{Name: "ACCESS MODES"},
|
||||
Header{Name: "STORAGECLASS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -96,8 +92,17 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
|
|||
capacity,
|
||||
accessModes,
|
||||
class,
|
||||
mapToStr(pvc.Labels),
|
||||
asStatus(p.diagnose(string(phase))),
|
||||
toAge(pvc.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PersistentVolumeClaim) diagnose(r string) error {
|
||||
if r != "Bound" && r != "Available" {
|
||||
return fmt.Errorf("unexpected status %s", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@ func (Rbac) Header(ns string) HeaderRow {
|
|||
Header{Name: "API GROUP"},
|
||||
}
|
||||
|
||||
return append(h, rbacVerbHeader()...)
|
||||
h = append(h, rbacVerbHeader()...)
|
||||
h = append(h, Header{Name: "VALID", Wide: true})
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// Render renders a K8s resource to screen.
|
||||
|
|
@ -57,8 +60,12 @@ func (Rbac) Render(o interface{}, gvr string, r *Row) error {
|
|||
}
|
||||
|
||||
r.ID = p.Resource
|
||||
r.Fields = append(r.Fields, cleanseResource(p.Resource), p.Group)
|
||||
r.Fields = append(r.Fields,
|
||||
cleanseResource(p.Resource),
|
||||
p.Group,
|
||||
)
|
||||
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
|
||||
r.Fields = append(r.Fields, "")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ func (Role) Header(ns string) HeaderRow {
|
|||
|
||||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -49,6 +51,8 @@ func (r Role) Render(o interface{}, ns string, row *Row) error {
|
|||
}
|
||||
row.Fields = append(row.Fields,
|
||||
ro.Name,
|
||||
mapToStr(ro.Labels),
|
||||
"",
|
||||
toAge(ro.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ func (RoleBinding) Header(ns string) HeaderRow {
|
|||
Header{Name: "ROLE"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "SUBJECTS"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -58,6 +60,8 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
|
|||
rb.RoleRef.Name,
|
||||
kind,
|
||||
ss,
|
||||
mapToStr(rb.Labels),
|
||||
"",
|
||||
toAge(rb.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
@ -87,11 +91,11 @@ func toSubjectAlias(s string) string {
|
|||
|
||||
switch s {
|
||||
case rbacv1.UserKind:
|
||||
return "USR"
|
||||
return "User"
|
||||
case rbacv1.GroupKind:
|
||||
return "GRP"
|
||||
return "Group"
|
||||
case rbacv1.ServiceAccountKind:
|
||||
return "SA"
|
||||
return "SvcAcct"
|
||||
default:
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestRoleBindingRender(t *testing.T) {
|
|||
c.Render(load(t, "rb"), "", &r)
|
||||
|
||||
assert.Equal(t, "default/blee", r.ID)
|
||||
assert.Equal(t, render.Fields{"default", "blee", "blee", "SA", "fernand"}, r.Fields[:5])
|
||||
assert.Equal(t, render.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ func (r Row) Clone() Row {
|
|||
}
|
||||
}
|
||||
|
||||
// Len returns the length of the row.
|
||||
func (r Row) Len() int {
|
||||
return len(r.Fields)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Rows represents a collection of rows.
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ func (rr RowEvents) Sort(ns string, col int, asc bool) {
|
|||
func toAgeDuration(dur string) string {
|
||||
d, err := time.ParseDuration(dur)
|
||||
if err != nil {
|
||||
return "n/a"
|
||||
return dur
|
||||
}
|
||||
return duration.HumanDuration(d)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ type Header struct {
|
|||
Name string
|
||||
Align int
|
||||
Decorator DecoratorFunc
|
||||
Hide bool
|
||||
Wide bool
|
||||
}
|
||||
|
||||
// Clone copies a header.
|
||||
|
|
@ -56,13 +58,7 @@ func (hh HeaderRow) Columns() []string {
|
|||
|
||||
// HasAge returns true if table has an age column.
|
||||
func (hh HeaderRow) HasAge() bool {
|
||||
for _, r := range hh {
|
||||
if r.Name == ageCol {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return hh.IndexOf(ageCol) != -1
|
||||
}
|
||||
|
||||
// AgeCol checks if given column index is the age column.
|
||||
|
|
@ -72,3 +68,18 @@ func (hh HeaderRow) AgeCol(col int) bool {
|
|||
}
|
||||
return col == len(hh)-1
|
||||
}
|
||||
|
||||
// ValidColIndex returns the valid col index or -1 if none.
|
||||
func (hh HeaderRow) ValidColIndex() int {
|
||||
return hh.IndexOf("VALID")
|
||||
}
|
||||
|
||||
// IndeOf returns the col index or -1 if none.
|
||||
func (hh HeaderRow) IndexOf(c string) int {
|
||||
for i, h := range hh {
|
||||
if h.Name == c {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/tview"
|
||||
|
|
@ -17,24 +16,19 @@ import (
|
|||
type ReplicaSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (ReplicaSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (r ReplicaSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
markCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
markCol--
|
||||
}
|
||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
return StdColor
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Header returns a header row.
|
||||
|
|
@ -49,6 +43,8 @@ func (ReplicaSet) Header(ns string) HeaderRow {
|
|||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||
Header{Name: "READY", Align: tview.AlignRight},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -75,8 +71,21 @@ func (s ReplicaSet) Render(o interface{}, ns string, r *Row) error {
|
|||
strconv.Itoa(int(*rs.Spec.Replicas)),
|
||||
strconv.Itoa(int(rs.Status.Replicas)),
|
||||
strconv.Itoa(int(rs.Status.ReadyReplicas)),
|
||||
mapToStr(rs.Labels),
|
||||
asStatus(s.diagnose(rs)),
|
||||
toAge(rs.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ReplicaSet) diagnose(rs appsv1.ReplicaSet) error {
|
||||
if rs.Status.Replicas != rs.Status.ReadyReplicas {
|
||||
if rs.Status.Replicas == 0 {
|
||||
return fmt.Errorf("did not phase down correctly expecting 0 replicas but got %d", rs.Status.ReadyReplicas)
|
||||
}
|
||||
return fmt.Errorf("mismatch desired(%d) vs ready(%d)", rs.Status.Replicas, rs.Status.ReadyReplicas)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ func (ServiceAccount) Header(ns string) HeaderRow {
|
|||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "SECRET"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -52,6 +54,8 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
|
|||
r.Fields = append(r.Fields,
|
||||
sa.Name,
|
||||
strconv.Itoa(len(sa.Secrets)),
|
||||
mapToStr(sa.Labels),
|
||||
"",
|
||||
toAge(sa.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ func (StorageClass) Header(ns string) HeaderRow {
|
|||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "PROVISIONER"},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +44,8 @@ func (StorageClass) Render(o interface{}, ns string, r *Row) error {
|
|||
r.Fields = Fields{
|
||||
sc.Name,
|
||||
string(sc.Provisioner),
|
||||
mapToStr(sc.Labels),
|
||||
"",
|
||||
toAge(sc.ObjectMeta.CreationTimestamp),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ var AgeDecorator = func(a string) string {
|
|||
func (ScreenDump) Header(ns string) HeaderRow {
|
||||
return HeaderRow{
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "DIR"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +49,8 @@ func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
|
|||
r.ID = filepath.Join(f.Dir, f.File.Name())
|
||||
r.Fields = Fields{
|
||||
f.File.Name(),
|
||||
f.Dir,
|
||||
"",
|
||||
timeToAge(f.File.ModTime()),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ func TestScreenDumpRender(t *testing.T) {
|
|||
assert.Equal(t, "fred/blee/bob", r.ID)
|
||||
assert.Equal(t, render.Fields{
|
||||
"bob",
|
||||
"fred/blee",
|
||||
"",
|
||||
}, r.Fields[:len(r.Fields)-1])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package render
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/gdamore/tcell"
|
||||
|
|
@ -16,20 +15,13 @@ import (
|
|||
type StatefulSet struct{}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (StatefulSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, r RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, r)
|
||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||
func (s StatefulSet) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
c := DefaultColorer(ns, re)
|
||||
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||
return c
|
||||
}
|
||||
|
||||
readyCol := 2
|
||||
if !client.IsAllNamespaces(ns) {
|
||||
readyCol--
|
||||
}
|
||||
tokens := strings.Split(strings.TrimSpace(r.Row.Fields[readyCol]), "/")
|
||||
curr, des := tokens[0], tokens[1]
|
||||
if curr != des {
|
||||
if !Happy(ns, re.Row) {
|
||||
return ErrColor
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +39,12 @@ func (StatefulSet) Header(ns string) HeaderRow {
|
|||
return append(h,
|
||||
Header{Name: "NAME"},
|
||||
Header{Name: "READY"},
|
||||
Header{Name: "SELECTOR"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "SERVICE"},
|
||||
Header{Name: "CONTAINERS", Wide: true},
|
||||
Header{Name: "IMAGES", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -72,11 +68,22 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
r.Fields = append(r.Fields,
|
||||
sts.Name,
|
||||
strconv.Itoa(int(sts.Status.Replicas))+"/"+strconv.Itoa(int(*sts.Spec.Replicas)),
|
||||
strconv.Itoa(int(sts.Status.ReadyReplicas))+"/"+strconv.Itoa(int(sts.Status.Replicas)),
|
||||
asSelector(sts.Spec.Selector),
|
||||
na(sts.Spec.ServiceName),
|
||||
podContainerNames(sts.Spec.Template.Spec, true),
|
||||
podImageNames(sts.Spec.Template.Spec, true),
|
||||
mapToStr(sts.Labels),
|
||||
asStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)),
|
||||
toAge(sts.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (StatefulSet) diagnose(d, r int32) error {
|
||||
if d != r {
|
||||
return fmt.Errorf("desiring %d replicas got %d available", d, r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestStatefulSetRender(t *testing.T) {
|
|||
|
||||
assert.Nil(t, c.Render(load(t, "sts"), "", &r))
|
||||
assert.Equal(t, "default/nginx-sts", r.ID)
|
||||
assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts"}, r.Fields[:len(r.Fields)-1])
|
||||
assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ import (
|
|||
// Subject renders a rbac to screen.
|
||||
type Subject struct{}
|
||||
|
||||
// Happy returns true if resoure is happy, false otherwise
|
||||
func (Subject) Happy(_ string, _ Row) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ColorerFunc colors a resource row.
|
||||
func (Subject) ColorerFunc() ColorerFunc {
|
||||
return func(ns string, re RowEvent) tcell.Color {
|
||||
|
|
@ -24,6 +29,7 @@ func (Subject) Header(ns string) HeaderRow {
|
|||
Header{Name: "NAME"},
|
||||
Header{Name: "KIND"},
|
||||
Header{Name: "FIRST LOCATION"},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,6 +46,7 @@ func (s Subject) Render(o interface{}, ns string, r *Row) error {
|
|||
res.Name,
|
||||
res.Kind,
|
||||
res.FirstLocation,
|
||||
"",
|
||||
)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -32,8 +32,10 @@ func (Service) Header(ns string) HeaderRow {
|
|||
Header{Name: "TYPE"},
|
||||
Header{Name: "CLUSTER-IP"},
|
||||
Header{Name: "EXTERNAL-IP"},
|
||||
Header{Name: "SELECTOR"},
|
||||
Header{Name: "PORTS"},
|
||||
Header{Name: "SELECTOR", Wide: true},
|
||||
Header{Name: "PORTS", Wide: true},
|
||||
Header{Name: "LABELS", Wide: true},
|
||||
Header{Name: "VALID", Wide: true},
|
||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||
)
|
||||
}
|
||||
|
|
@ -58,19 +60,32 @@ func (s Service) Render(o interface{}, ns string, r *Row) error {
|
|||
r.Fields = append(r.Fields,
|
||||
svc.ObjectMeta.Name,
|
||||
string(svc.Spec.Type),
|
||||
svc.Spec.ClusterIP,
|
||||
toIP(svc.Spec.ClusterIP),
|
||||
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
||||
mapToStr(svc.Spec.Selector),
|
||||
toPorts(svc.Spec.Ports),
|
||||
mapToStr(svc.Labels),
|
||||
asStatus(s.diagnose()),
|
||||
toAge(svc.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Service) diagnose() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func toIP(ip string) string {
|
||||
if ip == "" || ip == "None" {
|
||||
return ""
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func getSvcExtIPS(svc *v1.Service) []string {
|
||||
results := []string{}
|
||||
|
||||
|
|
@ -116,7 +131,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string {
|
|||
if svcType == v1.ServiceTypeLoadBalancer {
|
||||
return "<pending>"
|
||||
}
|
||||
return MissingValue
|
||||
return ""
|
||||
}
|
||||
sort.Strings(ips)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestServiceRender(t *testing.T) {
|
|||
c.Render(load(t, "svc"), "", &r)
|
||||
|
||||
assert.Equal(t, "default/dictionary1", r.ID)
|
||||
assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "<none>", "app=dictionary1", "http:4001►0"}, r.Fields[:7])
|
||||
assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
okColor, faultColor = tcell.ColorPaleGreen, tcell.ColorOrangeRed
|
||||
okColorName, faultColorName = "palegreen", "orangered"
|
||||
)
|
||||
|
||||
// Component represents a graphic component.
|
||||
type Component struct {
|
||||
*tview.Box
|
||||
|
||||
bgColor, noColor tcell.Color
|
||||
seriesColors []tcell.Color
|
||||
dimmed tcell.Style
|
||||
id, legend string
|
||||
blur func(tcell.Key)
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
// NewComponent returns a new component.
|
||||
func NewComponent(id string) *Component {
|
||||
return &Component{
|
||||
Box: tview.NewBox(),
|
||||
id: id,
|
||||
noColor: tcell.ColorBlack,
|
||||
seriesColors: []tcell.Color{tview.Styles.PrimaryTextColor, tview.Styles.FocusColor},
|
||||
dimmed: tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor).Foreground(tcell.ColorGray).Dim(true),
|
||||
}
|
||||
}
|
||||
|
||||
// SetBackgroundColor sets the graph bg color.
|
||||
func (c *Component) SetBackgroundColor(color tcell.Color) {
|
||||
c.Box.SetBackgroundColor(color)
|
||||
c.bgColor = color
|
||||
c.dimmed = c.dimmed.Background(color)
|
||||
}
|
||||
|
||||
// ID returns the component ID.
|
||||
func (c *Component) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
// SetLegend sets the component legend.
|
||||
func (c *Component) SetLegend(l string) {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
c.legend = l
|
||||
}
|
||||
|
||||
// InputHandler returns the handler for this primitive.
|
||||
func (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
switch key := event.Key(); key {
|
||||
case tcell.KeyEnter:
|
||||
log.Debug().Msgf("YO %s ENTER!!", c.id)
|
||||
case tcell.KeyBacktab, tcell.KeyTab:
|
||||
log.Debug().Msgf("YO %s TAB!!", c.id)
|
||||
if c.blur != nil {
|
||||
c.blur(key)
|
||||
}
|
||||
setFocus(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// IsDial returns true if chart is a dial
|
||||
func (c *Component) IsDial() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// SetBlurFunc sets a callback fn when component gets out of focus.
|
||||
func (c *Component) SetBlurFunc(handler func(key tcell.Key)) *Component {
|
||||
c.blur = handler
|
||||
return c
|
||||
}
|
||||
|
||||
// SetSeriesColors sets the component series colors.
|
||||
func (c *Component) SetSeriesColors(cc ...tcell.Color) {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
c.seriesColors = cc
|
||||
}
|
||||
|
||||
// GetSeriesColorNames returns series colors by name.
|
||||
func (c *Component) GetSeriesColorNames() []string {
|
||||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
|
||||
var nn []string
|
||||
for _, color := range c.seriesColors {
|
||||
for name, co := range tcell.ColorNames {
|
||||
if co == color {
|
||||
nn = append(nn, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(nn) < 2 {
|
||||
nn = append(nn, okColorName, faultColorName)
|
||||
}
|
||||
|
||||
return nn
|
||||
}
|
||||
|
||||
func (c *Component) colorForSeries() (tcell.Color, tcell.Color) {
|
||||
c.mx.RLock()
|
||||
defer c.mx.RUnlock()
|
||||
if len(c.seriesColors) > 1 {
|
||||
return c.seriesColors[0], c.seriesColors[1]
|
||||
}
|
||||
return okColor, faultColor
|
||||
}
|
||||
|
||||
func (c *Component) asRect() image.Rectangle {
|
||||
x, y, width, height := c.GetInnerRect()
|
||||
return image.Rectangle{
|
||||
Min: image.Point{X: x, Y: y},
|
||||
Max: image.Point{X: x + width, Y: y + height},
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// var dots = []rune{' ', '⠂', '⠶', '⠿'}
|
||||
var dots = []rune{' ', '⠂', '▤', '▥'}
|
||||
|
||||
// var dots = []rune{' ', '⠂', '▤', '▇'}
|
||||
|
||||
type Segment []int
|
||||
|
||||
type Segments []Segment
|
||||
|
||||
type Matrix [][]rune
|
||||
|
||||
type Orientation int
|
||||
|
||||
type DotMatrix struct {
|
||||
row, col int
|
||||
}
|
||||
|
||||
func NewDotMatrix(row, col int) DotMatrix {
|
||||
return DotMatrix{
|
||||
row: row,
|
||||
col: col,
|
||||
}
|
||||
}
|
||||
|
||||
func (d DotMatrix) Print(n int) Matrix {
|
||||
m := make(Matrix, d.row)
|
||||
segs := asSegments(n)
|
||||
for row := 0; row < d.row; row++ {
|
||||
for col := 0; col < d.col; col++ {
|
||||
m[row] = append(m[row], segs.CharFor(row, col))
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func asSegments(n int) Segment {
|
||||
switch n {
|
||||
case 0:
|
||||
return Segment{1, 1, 1, 0, 1, 1, 1}
|
||||
case 1:
|
||||
return Segment{0, 0, 1, 0, 0, 1, 0}
|
||||
case 2:
|
||||
return Segment{1, 0, 1, 1, 1, 0, 1}
|
||||
case 3:
|
||||
return Segment{1, 0, 1, 1, 0, 1, 1}
|
||||
case 4:
|
||||
return Segment{0, 1, 0, 1, 0, 1, 0}
|
||||
case 5:
|
||||
return Segment{1, 1, 0, 1, 0, 1, 1}
|
||||
case 6:
|
||||
return Segment{0, 1, 0, 1, 1, 1, 1}
|
||||
case 7:
|
||||
return Segment{1, 0, 1, 0, 0, 1, 0}
|
||||
case 8:
|
||||
return Segment{1, 1, 1, 1, 1, 1, 1}
|
||||
case 9:
|
||||
return Segment{1, 1, 1, 1, 0, 1, 0}
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("NYI %d", n))
|
||||
}
|
||||
}
|
||||
|
||||
func (s Segment) CharFor(row, col int) rune {
|
||||
c := ' '
|
||||
segs := ToSegments(row, col)
|
||||
if segs == nil {
|
||||
return c
|
||||
}
|
||||
for _, seg := range segs {
|
||||
if s[seg] == 1 {
|
||||
c = charForSeg(seg, row, col)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func charForSeg(seg, row, col int) rune {
|
||||
switch seg {
|
||||
case 0, 3, 6:
|
||||
return dots[2]
|
||||
}
|
||||
if row == 0 && (col == 0 || col == 2) {
|
||||
return dots[2]
|
||||
}
|
||||
|
||||
return dots[3]
|
||||
}
|
||||
|
||||
var segs = map[int][][]int{
|
||||
0: [][]int{[]int{1, 0}, []int{0}, []int{2, 0}},
|
||||
1: [][]int{[]int{1}, nil, []int{2}},
|
||||
2: [][]int{[]int{1, 3}, []int{3}, []int{2, 3}},
|
||||
3: [][]int{[]int{4}, nil, []int{5}},
|
||||
4: [][]int{[]int{4, 6}, []int{6}, []int{5, 6}},
|
||||
}
|
||||
|
||||
func ToSegments(row, col int) []int {
|
||||
return segs[row][col]
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package tchart_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/tchart"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSegmentFor(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
r, c int
|
||||
e []int
|
||||
}{
|
||||
"0x0": {r: 0, c: 0, e: []int{1, 0}},
|
||||
"0x1": {r: 0, c: 1, e: []int{0}},
|
||||
"0x2": {r: 0, c: 2, e: []int{2, 0}},
|
||||
"1x0": {r: 1, c: 0, e: []int{1}},
|
||||
"1x1": {r: 1, c: 1, e: nil},
|
||||
"1x2": {r: 1, c: 2, e: []int{2}},
|
||||
"2x0": {r: 2, c: 0, e: []int{1, 3}},
|
||||
"2x1": {r: 2, c: 1, e: []int{3}},
|
||||
"2x2": {r: 2, c: 2, e: []int{2, 3}},
|
||||
"3x0": {r: 3, c: 0, e: []int{4}},
|
||||
"3x1": {r: 3, c: 1, e: nil},
|
||||
"3x2": {r: 3, c: 2, e: []int{5}},
|
||||
"4x0": {r: 4, c: 0, e: []int{4, 6}},
|
||||
"4x1": {r: 4, c: 1, e: []int{6}},
|
||||
"4x2": {r: 4, c: 2, e: []int{5, 6}},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
assert.Equal(t, u.e, tchart.ToSegments(u.r, u.c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
d := tchart.NewDotMatrix(5, 3)
|
||||
for n := 0; n <= 9; n++ {
|
||||
i := n
|
||||
t.Run(strconv.Itoa(n), func(t *testing.T) {
|
||||
assert.Equal(t, numbers[i], d.Print(i))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
||||
const hChar, vChar = '⠶', '⠿'
|
||||
|
||||
var numbers = []tchart.Matrix{
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', vChar},
|
||||
{vChar, ' ', vChar},
|
||||
{vChar, ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
},
|
||||
[][]rune{
|
||||
{' ', ' ', hChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', ' '},
|
||||
{hChar, hChar, hChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, ' ', ' '},
|
||||
{vChar, ' ', ' '},
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', ' '},
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, ' ', ' '},
|
||||
{vChar, ' ', ' '},
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
},
|
||||
[][]rune{
|
||||
{hChar, hChar, hChar},
|
||||
{vChar, ' ', vChar},
|
||||
{hChar, hChar, hChar},
|
||||
{' ', ' ', vChar},
|
||||
{' ', ' ', vChar},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
DeltaSame delta = iota
|
||||
DeltaMore
|
||||
DeltaLess
|
||||
|
||||
gaugeFmt = "0%dd"
|
||||
)
|
||||
|
||||
type delta int
|
||||
|
||||
// Gauge represents a gauge component.
|
||||
type Gauge struct {
|
||||
*Component
|
||||
|
||||
data Metric
|
||||
deltaOk, deltaFault delta
|
||||
}
|
||||
|
||||
// NewGauge returns a new gauge.
|
||||
func NewGauge(id string) *Gauge {
|
||||
return &Gauge{
|
||||
Component: NewComponent(id),
|
||||
}
|
||||
}
|
||||
|
||||
// IsDial returns true if chart is a dial
|
||||
func (g *Gauge) IsDial() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Gauge) Add(m Metric) {
|
||||
g.mx.Lock()
|
||||
defer g.mx.Unlock()
|
||||
|
||||
g.deltaOk, g.deltaFault = computeDelta(g.data.OK, m.OK), computeDelta(g.data.Fault, m.Fault)
|
||||
g.data = m
|
||||
}
|
||||
|
||||
func (g *Gauge) drawNum(sc tcell.Screen, ok bool, o image.Point, n int, dn delta, ns string, style tcell.Style) {
|
||||
c1, _ := g.colorForSeries()
|
||||
if ok {
|
||||
o.X -= 1
|
||||
style = style.Foreground(c1)
|
||||
printDelta(sc, dn, o, style)
|
||||
o.X += 1
|
||||
}
|
||||
|
||||
dm, sig := NewDotMatrix(5, 3), n == 0
|
||||
for i := 0; i < len(ns); i++ {
|
||||
if ns[i] == '0' && !sig {
|
||||
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, g.dimmed)
|
||||
} else {
|
||||
sig = true
|
||||
g.drawDial(sc, dm.Print(int(ns[i]-48)), o, style)
|
||||
}
|
||||
o.X += 5
|
||||
}
|
||||
if !ok {
|
||||
printDelta(sc, dn, o, style)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gauge) Draw(sc tcell.Screen) {
|
||||
g.Component.Draw(sc)
|
||||
|
||||
g.mx.RLock()
|
||||
defer g.mx.RUnlock()
|
||||
|
||||
rect := g.asRect()
|
||||
mid := image.Point{X: rect.Min.X + rect.Dx()/2 - 2, Y: rect.Min.Y + rect.Dy()/2 - 2}
|
||||
|
||||
style := tcell.StyleDefault.Background(g.bgColor)
|
||||
style = style.Foreground(tcell.ColorYellow)
|
||||
sc.SetContent(mid.X+1, mid.Y+2, '⠔', nil, style)
|
||||
|
||||
var (
|
||||
max = g.data.MaxDigits()
|
||||
fmat = "%" + fmt.Sprintf(gaugeFmt, max)
|
||||
o = image.Point{X: mid.X - 3, Y: mid.Y}
|
||||
)
|
||||
|
||||
s1C, s2C := g.colorForSeries()
|
||||
d1, d2 := fmt.Sprintf(fmat, g.data.OK), fmt.Sprintf(fmat, g.data.Fault)
|
||||
o.X -= (len(d1) - 1) * 5
|
||||
g.drawNum(sc, true, o, g.data.OK, g.deltaOk, d1, style.Foreground(s1C).Dim(false))
|
||||
|
||||
o.X = mid.X + 3
|
||||
g.drawNum(sc, false, o, g.data.Fault, g.deltaFault, d2, style.Foreground(s2C).Dim(false))
|
||||
|
||||
if rect.Dx() > 0 && rect.Dy() > 0 && g.legend != "" {
|
||||
legend := g.legend
|
||||
if g.HasFocus() {
|
||||
legend = "[:aqua]" + g.legend + "[::]"
|
||||
}
|
||||
tview.Print(sc, legend, rect.Min.X, rect.Max.Y-1, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.Style) {
|
||||
for r := 0; r < len(m); r++ {
|
||||
for c := 0; c < len(m[r]); c++ {
|
||||
dot := m[r][c]
|
||||
if dot == dots[0] {
|
||||
sc.SetContent(o.X+c, o.Y+r, dots[1], nil, g.dimmed)
|
||||
} else {
|
||||
sc.SetContent(o.X+c, o.Y+r, dot, nil, style)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func computeDelta(d1, d2 int) delta {
|
||||
if d2 == 0 {
|
||||
return DeltaSame
|
||||
}
|
||||
|
||||
d := d2 - d1
|
||||
switch {
|
||||
case d > 0:
|
||||
return DeltaMore
|
||||
case d < 0:
|
||||
return DeltaLess
|
||||
default:
|
||||
return DeltaSame
|
||||
}
|
||||
}
|
||||
|
||||
func printDelta(sc tcell.Screen, d delta, o image.Point, s tcell.Style) {
|
||||
s = s.Dim(false)
|
||||
switch d {
|
||||
case DeltaLess:
|
||||
sc.SetContent(o.X-1, o.Y+2, '↓', nil, s)
|
||||
case DeltaMore:
|
||||
sc.SetContent(o.X-1, o.Y+2, '↑', nil, s)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package tchart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
type block struct {
|
||||
full int
|
||||
partial rune
|
||||
}
|
||||
|
||||
type blocks struct {
|
||||
oks, errs block
|
||||
}
|
||||
|
||||
// Metric tracks a good and error rates.
|
||||
type Metric struct {
|
||||
OK, Fault int
|
||||
}
|
||||
|
||||
// Max returns the max of the metric.
|
||||
func (m Metric) MaxDigits() int {
|
||||
max := int(math.Max(float64(m.OK), float64(m.Fault)))
|
||||
s := fmt.Sprintf("%d", max)
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// Sum returns the sum of the metrics.
|
||||
func (m Metric) Sum() int {
|
||||
return m.OK + m.Fault
|
||||
}
|
||||
|
||||
// Sparkline represents a sparkline component.
|
||||
type SparkLine struct {
|
||||
*Component
|
||||
|
||||
data []Metric
|
||||
lastWidth int
|
||||
}
|
||||
|
||||
// NewSparkLine returns a new graph.
|
||||
func NewSparkLine(id string) *SparkLine {
|
||||
return &SparkLine{
|
||||
Component: NewComponent(id),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a metric.
|
||||
func (s *SparkLine) Add(m Metric) {
|
||||
s.mx.Lock()
|
||||
defer s.mx.Unlock()
|
||||
s.data = append(s.data, m)
|
||||
}
|
||||
|
||||
// Draw draws the graph.
|
||||
func (s *SparkLine) Draw(screen tcell.Screen) {
|
||||
s.Component.Draw(screen)
|
||||
|
||||
s.mx.RLock()
|
||||
defer s.mx.RUnlock()
|
||||
|
||||
if len(s.data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pad := 1
|
||||
if s.legend != "" {
|
||||
pad++
|
||||
}
|
||||
|
||||
rect := s.asRect()
|
||||
s.lastWidth = rect.Dx()
|
||||
s.cutSet(rect.Dx())
|
||||
max := s.computeMax()
|
||||
|
||||
cX := rect.Min.X + 1
|
||||
if len(s.data) < rect.Dx() {
|
||||
cX = rect.Max.X - len(s.data)
|
||||
}
|
||||
|
||||
scale := float64(len(sparks)) * float64((rect.Dy() - pad)) / float64(max)
|
||||
|
||||
c1, c2 := s.colorForSeries()
|
||||
for _, d := range s.data {
|
||||
b := toBlocks(d, scale)
|
||||
cY := rect.Max.Y - pad
|
||||
cY = s.drawBlock(screen, cX, cY, b.oks, c1)
|
||||
s.drawBlock(screen, cX, cY, b.errs, c2)
|
||||
cX++
|
||||
}
|
||||
|
||||
if rect.Dx() > 0 && rect.Dy() > 0 && s.legend != "" {
|
||||
legend := s.legend
|
||||
if s.HasFocus() {
|
||||
legend = "[:aqua:]" + s.legend + "[::]"
|
||||
}
|
||||
tview.Print(screen, legend, rect.Min.X, rect.Max.Y-1, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SparkLine) drawBlock(screen tcell.Screen, x, y int, b block, c tcell.Color) int {
|
||||
style := tcell.StyleDefault.Foreground(c).Background(s.bgColor)
|
||||
|
||||
for i := 0; i < b.full; i++ {
|
||||
screen.SetContent(x, y, sparks[len(sparks)-1], nil, style)
|
||||
y--
|
||||
}
|
||||
if b.partial != 0 {
|
||||
screen.SetContent(x, y, b.partial, nil, style)
|
||||
}
|
||||
|
||||
return y
|
||||
}
|
||||
|
||||
func (s *SparkLine) cutSet(w int) {
|
||||
if w <= 0 || len(s.data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if w < len(s.data) {
|
||||
s.data = s.data[len(s.data)-w:]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SparkLine) computeMax() int {
|
||||
var max int
|
||||
for _, d := range s.data {
|
||||
sum := d.Sum()
|
||||
if sum > max {
|
||||
max = sum
|
||||
}
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
||||
|
||||
func toBlocks(value Metric, scale float64) blocks {
|
||||
if value.Sum() <= 0 {
|
||||
return blocks{}
|
||||
}
|
||||
|
||||
oks := int(math.Floor(float64(value.OK) * scale))
|
||||
part, okB := oks%len(sparks), block{full: oks / len(sparks)}
|
||||
if part > 0 {
|
||||
okB.partial = sparks[part-1]
|
||||
}
|
||||
|
||||
errs := int(math.Round(float64(value.Fault) * scale))
|
||||
part, errB := errs%len(sparks), block{full: errs / len(sparks)}
|
||||
if part > 0 {
|
||||
errB.partial = sparks[part-1]
|
||||
}
|
||||
|
||||
return blocks{oks: okB, errs: errB}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package ui
|
|||
import (
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -14,6 +15,7 @@ type App struct {
|
|||
Configurator
|
||||
|
||||
Main *Pages
|
||||
flash *model.Flash
|
||||
actions KeyActions
|
||||
views map[string]tview.Primitive
|
||||
cmdBuff *CmdBuff
|
||||
|
|
@ -25,6 +27,7 @@ func NewApp(context string) *App {
|
|||
Application: tview.NewApplication(),
|
||||
actions: make(KeyActions),
|
||||
Main: NewPages(),
|
||||
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||
}
|
||||
a.ReloadStyles(context)
|
||||
|
|
@ -33,7 +36,6 @@ func NewApp(context string) *App {
|
|||
"menu": NewMenu(a.Styles),
|
||||
"logo": NewLogo(a.Styles),
|
||||
"cmd": NewCommand(a.Styles),
|
||||
"flash": NewFlash(&a, "Initializing..."),
|
||||
"crumbs": NewCrumbs(a.Styles),
|
||||
}
|
||||
|
||||
|
|
@ -239,11 +241,6 @@ func (a *App) Logo() *Logo {
|
|||
return a.views["logo"].(*Logo)
|
||||
}
|
||||
|
||||
// Flash returns app flash.
|
||||
func (a *App) Flash() *Flash {
|
||||
return a.views["flash"].(*Flash)
|
||||
}
|
||||
|
||||
// Cmd returns app cmd.
|
||||
func (a *App) Cmd() *Command {
|
||||
return a.views["cmd"].(*Command)
|
||||
|
|
@ -254,6 +251,11 @@ func (a *App) Menu() *Menu {
|
|||
return a.views["menu"].(*Menu)
|
||||
}
|
||||
|
||||
// Flash returns a flash model.
|
||||
func (a *App) Flash() *model.Flash {
|
||||
return a.flash
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func TestAppViews(t *testing.T) {
|
|||
a := ui.NewApp("")
|
||||
a.Init()
|
||||
|
||||
vv := []string{"crumbs", "logo", "cmd", "flash", "menu"}
|
||||
vv := []string{"crumbs", "logo", "cmd", "menu"}
|
||||
for i := range vv {
|
||||
v := vv[i]
|
||||
t.Run(v, func(t *testing.T) {
|
||||
|
|
@ -68,7 +68,6 @@ func TestAppViews(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.NotNil(t, a.Crumbs())
|
||||
assert.NotNil(t, a.Flash())
|
||||
assert.NotNil(t, a.Logo())
|
||||
assert.NotNil(t, a.Cmd())
|
||||
assert.NotNil(t, a.Menu())
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ func (c *Configurator) RefreshStyles(context string) {
|
|||
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context))
|
||||
if c.Styles == nil {
|
||||
c.Styles = config.NewStyles()
|
||||
} else {
|
||||
c.Styles.Reset()
|
||||
}
|
||||
if err := c.Styles.Load(clusterSkins); err != nil {
|
||||
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
||||
|
|
@ -102,10 +104,10 @@ func (c *Configurator) updateStyles(f string) {
|
|||
}
|
||||
c.Styles.Update()
|
||||
|
||||
render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor)
|
||||
render.AddColor = config.AsColor(c.Styles.Frame().Status.AddColor)
|
||||
render.ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor)
|
||||
render.ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor)
|
||||
render.HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor)
|
||||
render.CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor)
|
||||
render.StdColor = c.Styles.Frame().Status.NewColor.Color()
|
||||
render.AddColor = c.Styles.Frame().Status.AddColor.Color()
|
||||
render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
|
||||
render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()
|
||||
render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()
|
||||
render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,51 +2,30 @@ package ui
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// FlashInfo represents an info message.
|
||||
FlashInfo FlashLevel = iota
|
||||
// FlashWarn represents an warning message.
|
||||
FlashWarn
|
||||
// FlashErr represents an error message.
|
||||
FlashErr
|
||||
// FlashFatal represents an fatal message.
|
||||
FlashFatal
|
||||
|
||||
flashDelay = 3 * time.Second
|
||||
|
||||
emoHappy = "😎"
|
||||
emoDoh = "😗"
|
||||
emoRed = "😡"
|
||||
emoDead = "💀"
|
||||
emoHappy = "😎"
|
||||
)
|
||||
|
||||
type (
|
||||
// FlashLevel represents flash message severity.
|
||||
FlashLevel int
|
||||
// Flash represents a flash message indicator.
|
||||
type Flash struct {
|
||||
*tview.TextView
|
||||
|
||||
// Flash represents a flash message indicator.
|
||||
Flash struct {
|
||||
*tview.TextView
|
||||
|
||||
cancel context.CancelFunc
|
||||
app *App
|
||||
flushNow bool
|
||||
}
|
||||
)
|
||||
app *App
|
||||
testMode bool
|
||||
}
|
||||
|
||||
// NewFlash returns a new flash view.
|
||||
func NewFlash(app *App, m string) *Flash {
|
||||
func NewFlash(app *App) *Flash {
|
||||
f := Flash{
|
||||
app: app,
|
||||
TextView: tview.NewTextView(),
|
||||
|
|
@ -54,15 +33,14 @@ func NewFlash(app *App, m string) *Flash {
|
|||
f.SetTextColor(tcell.ColorAqua)
|
||||
f.SetTextAlign(tview.AlignLeft)
|
||||
f.SetBorderPadding(0, 0, 1, 1)
|
||||
f.SetText(m)
|
||||
f.app.Styles.AddListener(&f)
|
||||
|
||||
return &f
|
||||
}
|
||||
|
||||
// TestMode for testing...
|
||||
func (f *Flash) TestMode() {
|
||||
f.flushNow = true
|
||||
// SetTestMode for testing ONLY!
|
||||
func (f *Flash) SetTestMode(b bool) {
|
||||
f.testMode = b
|
||||
}
|
||||
|
||||
// StylesChanged notifies listener the skin changed.
|
||||
|
|
@ -71,101 +49,53 @@ func (f *Flash) StylesChanged(s *config.Styles) {
|
|||
f.SetTextColor(s.FgColor())
|
||||
}
|
||||
|
||||
// Info displays an info flash message.
|
||||
func (f *Flash) Info(msg string) {
|
||||
log.Info().Msg(msg)
|
||||
f.SetMessage(FlashInfo, msg)
|
||||
}
|
||||
|
||||
// Infof displays a formatted info flash message.
|
||||
func (f *Flash) Infof(fmat string, args ...interface{}) {
|
||||
f.Info(fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// Warn displays a warning flash message.
|
||||
func (f *Flash) Warn(msg string) {
|
||||
log.Warn().Msg(msg)
|
||||
f.SetMessage(FlashWarn, msg)
|
||||
}
|
||||
|
||||
// Warnf displays a formatted warning flash message.
|
||||
func (f *Flash) Warnf(fmat string, args ...interface{}) {
|
||||
f.Warn(fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// Err displays an error flash message.
|
||||
func (f *Flash) Err(err error) {
|
||||
log.Error().Msg(err.Error())
|
||||
f.SetMessage(FlashErr, err.Error())
|
||||
}
|
||||
|
||||
// Errf displays a formatted error flash message.
|
||||
func (f *Flash) Errf(fmat string, args ...interface{}) {
|
||||
var err error
|
||||
for _, a := range args {
|
||||
switch e := a.(type) {
|
||||
case error:
|
||||
err = e
|
||||
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
|
||||
defer log.Debug().Msgf("Flash Canceled!")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg := <-c:
|
||||
f.SetMessage(msg)
|
||||
}
|
||||
}
|
||||
log.Error().Err(err).Msgf(fmat, args...)
|
||||
f.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))
|
||||
}
|
||||
|
||||
// SetMessage sets flash message and level.
|
||||
func (f *Flash) SetMessage(level FlashLevel, msg ...string) {
|
||||
if f.cancel != nil {
|
||||
f.cancel()
|
||||
func (f *Flash) SetMessage(m model.LevelMessage) {
|
||||
fn := func() {
|
||||
if m.Text == "" {
|
||||
f.Clear()
|
||||
return
|
||||
}
|
||||
f.SetTextColor(flashColor(m.Level))
|
||||
f.SetText(flashEmoji(m.Level) + " " + m.Text)
|
||||
}
|
||||
|
||||
_, _, width, _ := f.GetRect()
|
||||
if width <= 15 {
|
||||
width = 100
|
||||
}
|
||||
m := strings.Join(msg, " ")
|
||||
if f.flushNow {
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
if f.testMode {
|
||||
fn()
|
||||
} else {
|
||||
f.app.QueueUpdateDraw(func() {
|
||||
f.SetTextColor(flashColor(level))
|
||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
||||
})
|
||||
f.app.QueueUpdateDraw(fn)
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, f.cancel = context.WithTimeout(context.Background(), flashDelay)
|
||||
go f.refresh(ctx)
|
||||
}
|
||||
|
||||
func (f *Flash) refresh(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
f.app.QueueUpdateDraw(func() {
|
||||
f.Clear()
|
||||
})
|
||||
}
|
||||
|
||||
func flashEmoji(l FlashLevel) string {
|
||||
func flashEmoji(l model.FlashLevel) string {
|
||||
switch l {
|
||||
case FlashWarn:
|
||||
case model.FlashWarn:
|
||||
return emoDoh
|
||||
case FlashErr:
|
||||
case model.FlashErr:
|
||||
return emoRed
|
||||
case FlashFatal:
|
||||
return emoDead
|
||||
default:
|
||||
return emoHappy
|
||||
}
|
||||
}
|
||||
|
||||
func flashColor(l FlashLevel) tcell.Color {
|
||||
func flashColor(l model.FlashLevel) tcell.Color {
|
||||
switch l {
|
||||
case FlashWarn:
|
||||
case model.FlashWarn:
|
||||
return tcell.ColorOrange
|
||||
case FlashErr:
|
||||
case model.FlashErr:
|
||||
return tcell.ColorOrangeRed
|
||||
case FlashFatal:
|
||||
return tcell.ColorFuchsia
|
||||
default:
|
||||
return tcell.ColorNavajoWhite
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,40 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFlashInfo(t *testing.T) {
|
||||
f := newFlash()
|
||||
f.Info("Blee")
|
||||
func TestFlash(t *testing.T) {
|
||||
const delay = 1 * time.Millisecond
|
||||
uu := map[string]struct {
|
||||
l model.FlashLevel
|
||||
i, e string
|
||||
}{
|
||||
"info": {l: model.FlashInfo, i: "hello", e: "😎 hello\n"},
|
||||
"warn": {l: model.FlashWarn, i: "hello", e: "😗 hello\n"},
|
||||
"err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"},
|
||||
}
|
||||
|
||||
assert.Equal(t, "😎 Blee\n", f.GetText(false))
|
||||
f.Infof("Blee %s", "duh")
|
||||
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashWarn(t *testing.T) {
|
||||
f := newFlash()
|
||||
f.Warn("Blee")
|
||||
|
||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
||||
f.Warnf("Blee %s", "duh")
|
||||
assert.Equal(t, "😗 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
func TestFlashErr(t *testing.T) {
|
||||
f := newFlash()
|
||||
|
||||
f.Err(errors.New("Blee"))
|
||||
assert.Equal(t, "😡 Blee\n", f.GetText(false))
|
||||
f.Errf("Blee %s", "duh")
|
||||
assert.Equal(t, "😡 Blee duh\n", f.GetText(false))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func newFlash() *ui.Flash {
|
||||
f := ui.NewFlash(ui.NewApp(""), "YO!")
|
||||
f.TestMode()
|
||||
return f
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
a := ui.NewApp("test")
|
||||
f := ui.NewFlash(a)
|
||||
f.SetTestMode(true)
|
||||
go f.Watch(ctx, a.Flash().Channel())
|
||||
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
a.Flash().SetMessage(u.l, u.i)
|
||||
time.Sleep(delay)
|
||||
assert.Equal(t, u.e, f.GetText(false))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,30 +60,30 @@ func (l *Logo) Reset() {
|
|||
|
||||
// Err displays a log error state.
|
||||
func (l *Logo) Err(msg string) {
|
||||
l.update(msg, "red")
|
||||
l.update(msg, config.NewColor("red"))
|
||||
}
|
||||
|
||||
// Warn displays a log warning state.
|
||||
func (l *Logo) Warn(msg string) {
|
||||
l.update(msg, "mediumvioletred")
|
||||
l.update(msg, config.NewColor("mediumvioletred"))
|
||||
}
|
||||
|
||||
// Info displays a log info state.
|
||||
func (l *Logo) Info(msg string) {
|
||||
l.update(msg, "green")
|
||||
l.update(msg, config.NewColor("green"))
|
||||
}
|
||||
|
||||
func (l *Logo) update(msg, c string) {
|
||||
func (l *Logo) update(msg string, c config.Color) {
|
||||
l.refreshStatus(msg, c)
|
||||
l.refreshLogo(c)
|
||||
}
|
||||
|
||||
func (l *Logo) refreshStatus(msg, c string) {
|
||||
l.status.SetBackgroundColor(config.AsColor(c))
|
||||
func (l *Logo) refreshStatus(msg string, c config.Color) {
|
||||
l.status.SetBackgroundColor(c.Color())
|
||||
l.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
||||
}
|
||||
|
||||
func (l *Logo) refreshLogo(c string) {
|
||||
func (l *Logo) refreshLogo(c config.Color) {
|
||||
l.logo.Clear()
|
||||
for i, s := range LogoSmall {
|
||||
fmt.Fprintf(l.logo, "[%s::b]%s", c, s)
|
||||
|
|
|
|||
|
|
@ -188,16 +188,16 @@ func toMnemonic(s string) string {
|
|||
}
|
||||
|
||||
func formatNSMenu(i int, name string, styles config.Frame) string {
|
||||
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
||||
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":", -1)
|
||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1)
|
||||
return fmt.Sprintf(fmat, i, name)
|
||||
}
|
||||
|
||||
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
|
||||
menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:d]%s "
|
||||
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1)
|
||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
||||
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":", -1)
|
||||
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ type Table struct {
|
|||
sortCol SortColumn
|
||||
colorerFn render.ColorerFunc
|
||||
decorateFn DecorateFunc
|
||||
wide bool
|
||||
toast bool
|
||||
}
|
||||
|
||||
// NewTable returns a new table view.
|
||||
|
|
@ -65,6 +67,7 @@ func (t *Table) Init(ctx context.Context) {
|
|||
t.SetSelectable(true, false)
|
||||
t.SetSelectionChangedFunc(t.selectionChanged)
|
||||
t.SetInputCapture(t.keyboard)
|
||||
t.SetBackgroundColor(tcell.ColorDefault)
|
||||
|
||||
t.styles = mustExtractSyles(ctx)
|
||||
t.StylesChanged(t.styles)
|
||||
|
|
@ -72,17 +75,35 @@ func (t *Table) Init(ctx context.Context) {
|
|||
|
||||
// StylesChanged notifies the skin changed.
|
||||
func (t *Table) StylesChanged(s *config.Styles) {
|
||||
t.SetBackgroundColor(config.AsColor(s.Table().BgColor))
|
||||
t.SetBorderColor(config.AsColor(s.Table().FgColor))
|
||||
t.SetBorderFocusColor(config.AsColor(s.Frame().Border.FocusColor))
|
||||
t.SetBackgroundColor(s.Table().BgColor.Color())
|
||||
t.SetBorderColor(s.Table().FgColor.Color())
|
||||
t.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())
|
||||
t.SetSelectedStyle(
|
||||
tcell.ColorBlack,
|
||||
config.AsColor(t.styles.Table().CursorColor),
|
||||
t.styles.Table().CursorColor.Color(),
|
||||
tcell.AttrBold,
|
||||
)
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// ResetToast resets toast flag.
|
||||
func (t *Table) ResetToast() {
|
||||
t.toast = false
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// ToggleToast toggles to show toast resources.
|
||||
func (t *Table) ToggleToast() {
|
||||
t.toast = !t.toast
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// ToggleWide toggles wide col display.
|
||||
func (t *Table) ToggleWide() {
|
||||
t.wide = !t.wide
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
// Actions returns active menu bindings.
|
||||
func (t *Table) Actions() KeyActions {
|
||||
return t.actions
|
||||
|
|
@ -166,10 +187,7 @@ func (t *Table) Update(data render.TableData) {
|
|||
if t.decorateFn != nil {
|
||||
data = t.decorateFn(data)
|
||||
}
|
||||
if !t.cmdBuff.Empty() {
|
||||
data = t.filtered(data)
|
||||
}
|
||||
t.doUpdate(data)
|
||||
t.doUpdate(t.filtered(data))
|
||||
t.UpdateTitle()
|
||||
}
|
||||
|
||||
|
|
@ -182,13 +200,18 @@ func (t *Table) doUpdate(data render.TableData) {
|
|||
|
||||
t.Clear()
|
||||
t.adjustSorter(data)
|
||||
fg := config.AsColor(t.styles.Table().Header.FgColor)
|
||||
bg := config.AsColor(t.styles.Table().Header.BgColor)
|
||||
for col, h := range data.Header {
|
||||
fg := t.styles.Table().Header.FgColor.Color()
|
||||
bg := t.styles.Table().Header.BgColor.Color()
|
||||
var col int
|
||||
for _, h := range data.Header {
|
||||
if h.Wide && !t.wide {
|
||||
continue
|
||||
}
|
||||
t.AddHeaderCell(col, h)
|
||||
c := t.GetCell(0, col)
|
||||
c.SetBackgroundColor(bg)
|
||||
c.SetTextColor(fg)
|
||||
col++
|
||||
}
|
||||
data.RowEvents.Sort(data.Namespace, t.sortCol.index, t.sortCol.asc)
|
||||
|
||||
|
|
@ -209,6 +232,8 @@ func (t *Table) SortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.E
|
|||
index = 0
|
||||
case -1:
|
||||
index = t.GetColumnCount() - 1
|
||||
case -3:
|
||||
index = t.GetColumnCount() - 2
|
||||
default:
|
||||
index = t.NameColIndex() + col
|
||||
}
|
||||
|
|
@ -251,29 +276,33 @@ func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.Hea
|
|||
color = t.colorerFn
|
||||
}
|
||||
marked := t.IsMarked(re.Row.ID)
|
||||
for col, field := range re.Row.Fields {
|
||||
if !re.Deltas.IsBlank() && !header.AgeCol(col) {
|
||||
field += Deltas(re.Deltas[col], field)
|
||||
var col int
|
||||
for c, field := range re.Row.Fields {
|
||||
if header[c].Wide && !t.wide {
|
||||
continue
|
||||
}
|
||||
if !re.Deltas.IsBlank() && !header.AgeCol(c) {
|
||||
field += Deltas(re.Deltas[c], field)
|
||||
}
|
||||
if header[c].Decorator != nil {
|
||||
field = header[c].Decorator(field)
|
||||
}
|
||||
if header[c].Align == tview.AlignLeft {
|
||||
field = formatCell(field, pads[c])
|
||||
}
|
||||
|
||||
if header[col].Decorator != nil {
|
||||
field = header[col].Decorator(field)
|
||||
}
|
||||
|
||||
if header[col].Align == tview.AlignLeft {
|
||||
field = formatCell(field, pads[col])
|
||||
}
|
||||
c := tview.NewTableCell(field)
|
||||
c.SetExpansion(1)
|
||||
c.SetAlign(header[col].Align)
|
||||
c.SetTextColor(color(ns, re))
|
||||
cell := tview.NewTableCell(field)
|
||||
cell.SetExpansion(1)
|
||||
cell.SetAlign(header[c].Align)
|
||||
cell.SetTextColor(color(ns, re))
|
||||
if marked {
|
||||
c.SetTextColor(config.AsColor(t.styles.Table().MarkColor))
|
||||
cell.SetTextColor(t.styles.Table().MarkColor.Color())
|
||||
}
|
||||
if col == 0 {
|
||||
c.SetReference(re.Row.ID)
|
||||
cell.SetReference(re.Row.ID)
|
||||
}
|
||||
t.SetCell(r, col, c)
|
||||
t.SetCell(r, col, cell)
|
||||
col++
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -298,6 +327,9 @@ func (t *Table) GetSelectedRow() render.Row {
|
|||
// NameColIndex returns the index of the resource name column.
|
||||
func (t *Table) NameColIndex() int {
|
||||
col := 0
|
||||
if client.IsClusterScoped(t.GetModel().GetNamespace()) {
|
||||
return col
|
||||
}
|
||||
if t.GetModel().ClusterWide() {
|
||||
col++
|
||||
}
|
||||
|
|
@ -313,20 +345,25 @@ func (t *Table) AddHeaderCell(col int, h render.Header) {
|
|||
}
|
||||
|
||||
func (t *Table) filtered(data render.TableData) render.TableData {
|
||||
if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) {
|
||||
return data
|
||||
filtered := data
|
||||
if t.toast {
|
||||
filtered = filterToast(data)
|
||||
}
|
||||
q := t.cmdBuff.String()
|
||||
if IsFuzzySelector(q) {
|
||||
return fuzzyFilter(q[2:], t.NameColIndex(), data)
|
||||
if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) {
|
||||
return filtered
|
||||
}
|
||||
|
||||
filtered, err := rxFilter(t.cmdBuff.String(), data)
|
||||
q := t.cmdBuff.String()
|
||||
if IsFuzzySelector(q) {
|
||||
return fuzzyFilter(q[2:], t.NameColIndex(), filtered)
|
||||
}
|
||||
|
||||
filtered, err := rxFilter(t.cmdBuff.String(), filtered)
|
||||
if err != nil {
|
||||
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
|
||||
t.cmdBuff.Clear()
|
||||
return data
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,15 +85,15 @@ func TrimLabelSelector(s string) string {
|
|||
// SkinTitle decorates a title.
|
||||
func SkinTitle(fmat string, style config.Frame) string {
|
||||
bgColor := style.Title.BgColor
|
||||
if bgColor == "default" {
|
||||
bgColor = "-"
|
||||
if bgColor == config.DefaultColor {
|
||||
bgColor = config.TransparentColor
|
||||
}
|
||||
fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+bgColor, -1)
|
||||
fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1)
|
||||
fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1)
|
||||
fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor, 1)
|
||||
fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor, 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+bgColor+":", -1)
|
||||
fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor.String()+":"+bgColor.String(), -1)
|
||||
fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor.String(), 1)
|
||||
fmat = strings.Replace(fmat, ":bg:", ":"+bgColor.String()+":", -1)
|
||||
|
||||
return fmat
|
||||
}
|
||||
|
|
@ -118,6 +118,25 @@ func formatCell(field string, padding int) string {
|
|||
return field
|
||||
}
|
||||
|
||||
func filterToast(data render.TableData) render.TableData {
|
||||
validX := data.Header.IndexOf("VALID")
|
||||
if validX == -1 {
|
||||
return data
|
||||
}
|
||||
|
||||
toast := render.TableData{
|
||||
Header: data.Header,
|
||||
RowEvents: make(render.RowEvents, 0, len(data.RowEvents)),
|
||||
Namespace: data.Namespace,
|
||||
}
|
||||
for _, re := range data.RowEvents {
|
||||
if re.Row.Fields[validX] != "" {
|
||||
toast.RowEvents = append(toast.RowEvents, re)
|
||||
}
|
||||
}
|
||||
return toast
|
||||
}
|
||||
|
||||
func rxFilter(q string, data render.TableData) (render.TableData, error) {
|
||||
rx, err := regexp.Compile(`(?i)` + q)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ func (t *testModel) Peek() render.TableData { return makeTableData() }
|
|||
func (t *testModel) ClusterWide() bool { return false }
|
||||
func (t *testModel) GetNamespace() string { return "blee" }
|
||||
func (t *testModel) SetNamespace(string) {}
|
||||
func (t *testModel) ToggleToast() {}
|
||||
func (t *testModel) AddListener(model.TableListener) {}
|
||||
func (t *testModel) Watch(context.Context) {}
|
||||
func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue