checkpoint
parent
a76d5809b8
commit
355f4ce396
|
|
@ -16,6 +16,7 @@ builds:
|
||||||
- 386
|
- 386
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
- armhf
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
- 6
|
||||||
- 7
|
- 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/gdamore/tcell v1.3.0
|
||||||
github.com/ghodss/yaml v1.0.0
|
github.com/ghodss/yaml v1.0.0
|
||||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
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/mattn/go-runewidth v0.0.5
|
||||||
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
||||||
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
||||||
github.com/openfaas/faas-provider v0.15.0
|
github.com/openfaas/faas-provider v0.15.0
|
||||||
github.com/petergtz/pegomock v2.6.0+incompatible
|
github.com/petergtz/pegomock v2.6.0+incompatible
|
||||||
github.com/rakyll/hey v0.1.2
|
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/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v0.0.5
|
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 h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
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/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 h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
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=
|
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 h1:XlGaKcBdmXJaPImiTnE+TGLDUWQ2toYuHCwdrylLjmg=
|
||||||
github.com/rakyll/hey v0.1.2/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608=
|
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/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/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.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
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/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 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||||
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
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/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 v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,46 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
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.
|
// MetricsServer serves cluster metrics for nodes and pods.
|
||||||
type MetricsServer struct {
|
type MetricsServer struct {
|
||||||
Connection
|
Connection
|
||||||
|
|
||||||
|
cache *cache.LRUExpireCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMetricsServer return a metric server instance.
|
// NewMetricsServer return a metric server instance.
|
||||||
func NewMetricsServer(c Connection) *MetricsServer {
|
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.
|
// 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 {
|
for _, no := range nodes.Items {
|
||||||
mmx[no.Name] = NodeMetrics{
|
mmx[no.Name] = NodeMetrics{
|
||||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
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(),
|
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 {
|
for _, c := range metrics.Items {
|
||||||
if mx, ok := mmx[c.Name]; ok {
|
if mx, ok := mmx[c.Name]; ok {
|
||||||
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
||||||
mx.CurrentMEM = toMB(c.Usage.Memory().Value())
|
mx.CurrentMEM = ToMB(c.Usage.Memory().Value())
|
||||||
mmx[c.Name] = mx
|
mmx[c.Name] = mx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,13 +77,13 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
for _, no := range nos.Items {
|
for _, no := range nos.Items {
|
||||||
nodeMetrics[no.Name] = NodeMetrics{
|
nodeMetrics[no.Name] = NodeMetrics{
|
||||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
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 {
|
for _, mx := range nmx.Items {
|
||||||
if m, ok := nodeMetrics[mx.Name]; ok {
|
if m, ok := nodeMetrics[mx.Name]; ok {
|
||||||
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
||||||
m.CurrentMEM = toMB(mx.Usage.Memory().Value())
|
m.CurrentMEM = ToMB(mx.Usage.Memory().Value())
|
||||||
nodeMetrics[mx.Name] = m
|
nodeMetrics[mx.Name] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,86 +100,121 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
||||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
|
||||||
var mx mv1beta1.NodeMetricsList
|
|
||||||
if !m.HasMetrics() {
|
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 {
|
if err != nil {
|
||||||
return &mx, err
|
return err
|
||||||
}
|
}
|
||||||
if !auth {
|
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()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
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.
|
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||||
var mx mv1beta1.PodMetricsList
|
mx := new(mv1beta1.PodMetricsList)
|
||||||
if m.Connection == nil {
|
const msg = "user is not authorized to list pods metrics"
|
||||||
return &mx, fmt.Errorf("no client connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.HasMetrics() {
|
|
||||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
|
||||||
}
|
|
||||||
if ns == NamespaceAll {
|
if ns == NamespaceAll {
|
||||||
ns = AllNamespaces
|
ns = AllNamespaces
|
||||||
}
|
}
|
||||||
|
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", ListAccess)
|
return mx, err
|
||||||
if 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()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
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.
|
// FetchPodMetrics return all metrics for pods in a given namespace.
|
||||||
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
|
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
|
||||||
var mx mv1beta1.PodMetrics
|
var mx *mv1beta1.PodMetrics
|
||||||
if m.Connection == nil {
|
const msg = "user is not authorized to list pod metrics"
|
||||||
return &mx, fmt.Errorf("no client connection")
|
|
||||||
}
|
|
||||||
if !m.HasMetrics() {
|
|
||||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
|
||||||
}
|
|
||||||
|
|
||||||
ns, n := Namespaced(fqn)
|
ns, n := Namespaced(fqn)
|
||||||
if ns == NamespaceAll {
|
if ns == NamespaceAll {
|
||||||
ns = AllNamespaces
|
ns = AllNamespaces
|
||||||
}
|
}
|
||||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", GetAccess)
|
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
|
||||||
if err != nil {
|
return mx, err
|
||||||
return &mx, err
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !auth {
|
|
||||||
return &mx, fmt.Errorf("user is not authorized to list pod metrics")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := m.MXDial()
|
client, err := m.MXDial()
|
||||||
if err != nil {
|
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.
|
// 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
|
var mx PodMetrics
|
||||||
for _, c := range p.Containers {
|
for _, c := range p.Containers {
|
||||||
mx.CurrentCPU += c.Usage.Cpu().MilliValue()
|
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
|
mmx[p.Namespace+"/"+p.Name] = mx
|
||||||
}
|
}
|
||||||
|
|
@ -178,8 +239,8 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
|
||||||
|
|
||||||
const megaByte = 1024 * 1024
|
const megaByte = 1024 * 1024
|
||||||
|
|
||||||
// toMB converts bytes to megabytes.
|
// ToMB converts bytes to megabytes.
|
||||||
func toMB(v int64) float64 {
|
func ToMB(v int64) float64 {
|
||||||
return float64(v) / megaByte
|
return float64(v) / megaByte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ const (
|
||||||
|
|
||||||
// ClusterScope designates a resource is not namespaced.
|
// ClusterScope designates a resource is not namespaced.
|
||||||
ClusterScope = "-"
|
ClusterScope = "-"
|
||||||
|
|
||||||
|
// NotNamespaced designates a non resource namespace.
|
||||||
|
NotNamespaced = "*"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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.
|
// ShortNames return all shortnames.
|
||||||
func (a *Aliases) ShortNames() ShortNames {
|
func (a *Aliases) ShortNames() ShortNames {
|
||||||
a.mx.RLock()
|
a.mx.RLock()
|
||||||
|
|
@ -139,8 +80,14 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAliases loads alias from a given file.
|
// Load K9s aliases.
|
||||||
func (a *Aliases) LoadAliases(path string) error {
|
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)
|
f, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msgf("No custom aliases found")
|
log.Debug().Err(err).Msgf("No custom aliases found")
|
||||||
|
|
@ -161,6 +108,63 @@ func (a *Aliases) LoadAliases(path string) error {
|
||||||
return nil
|
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.
|
// Save alias to disk.
|
||||||
func (a *Aliases) Save() error {
|
func (a *Aliases) Save() error {
|
||||||
log.Debug().Msg("[Config] Saving Aliases...")
|
log.Debug().Msg("[Config] Saving Aliases...")
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ func TestAliasDefine(t *testing.T) {
|
||||||
func TestAliasesLoad(t *testing.T) {
|
func TestAliasesLoad(t *testing.T) {
|
||||||
a := config.NewAliases()
|
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))
|
assert.Equal(t, 2, len(a.Alias))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +82,6 @@ func TestAliasesSave(t *testing.T) {
|
||||||
a.Alias["blee"] = "duh"
|
a.Alias["blee"] = "duh"
|
||||||
|
|
||||||
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
|
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))
|
assert.Equal(t, 2, len(a.Alias))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,31 @@ type StyleListener interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// Color represents a color.
|
||||||
|
Color string
|
||||||
|
|
||||||
|
// Colors tracks multiple colors.
|
||||||
|
Colors []Color
|
||||||
|
|
||||||
// Styles tracks K9s styling options.
|
// Styles tracks K9s styling options.
|
||||||
Styles struct {
|
Styles struct {
|
||||||
K9s Style `yaml:"k9s"`
|
K9s Style `yaml:"k9s"`
|
||||||
listeners []StyleListener
|
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 tracks body styles.
|
||||||
Body struct {
|
Body struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
LogoColor string `yaml:"logoColor"`
|
LogoColor Color `yaml:"logoColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame tracks frame styles.
|
// Frame tracks frame styles.
|
||||||
|
|
@ -45,118 +59,169 @@ type (
|
||||||
|
|
||||||
// Views tracks individual view styles.
|
// Views tracks individual view styles.
|
||||||
Views struct {
|
Views struct {
|
||||||
|
Table Table `yaml:"table"`
|
||||||
|
Xray Xray `yaml:"xray"`
|
||||||
|
Charts Charts `yaml:"charts"`
|
||||||
Yaml Yaml `yaml:"yaml"`
|
Yaml Yaml `yaml:"yaml"`
|
||||||
Log Log `yaml:"logs"`
|
Log Log `yaml:"logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status tracks resource status styles.
|
// Status tracks resource status styles.
|
||||||
Status struct {
|
Status struct {
|
||||||
NewColor string `yaml:"newColor"`
|
NewColor Color `yaml:"newColor"`
|
||||||
ModifyColor string `yaml:"modifyColor"`
|
ModifyColor Color `yaml:"modifyColor"`
|
||||||
AddColor string `yaml:"addColor"`
|
AddColor Color `yaml:"addColor"`
|
||||||
ErrorColor string `yaml:"errorColor"`
|
ErrorColor Color `yaml:"errorColor"`
|
||||||
HighlightColor string `yaml:"highlightColor"`
|
HighlightColor Color `yaml:"highlightColor"`
|
||||||
KillColor string `yaml:"killColor"`
|
KillColor Color `yaml:"killColor"`
|
||||||
CompletedColor string `yaml:"completedColor"`
|
CompletedColor Color `yaml:"completedColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log tracks Log styles.
|
// Log tracks Log styles.
|
||||||
Log struct {
|
Log struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yaml tracks yaml styles.
|
// Yaml tracks yaml styles.
|
||||||
Yaml struct {
|
Yaml struct {
|
||||||
KeyColor string `yaml:"keyColor"`
|
KeyColor Color `yaml:"keyColor"`
|
||||||
ValueColor string `yaml:"valueColor"`
|
ValueColor Color `yaml:"valueColor"`
|
||||||
ColonColor string `yaml:"colonColor"`
|
ColonColor Color `yaml:"colonColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title tracks title styles.
|
// Title tracks title styles.
|
||||||
Title struct {
|
Title struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
HighlightColor string `yaml:"highlightColor"`
|
HighlightColor Color `yaml:"highlightColor"`
|
||||||
CounterColor string `yaml:"counterColor"`
|
CounterColor Color `yaml:"counterColor"`
|
||||||
FilterColor string `yaml:"filterColor"`
|
FilterColor Color `yaml:"filterColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info tracks info styles.
|
// Info tracks info styles.
|
||||||
Info struct {
|
Info struct {
|
||||||
SectionColor string `yaml:"sectionColor"`
|
SectionColor Color `yaml:"sectionColor"`
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border tracks border styles.
|
// ColorBorder tracks border styles.
|
||||||
Border struct {
|
Border struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
FocusColor string `yaml:"focusColor"`
|
FocusColor Color `yaml:"focusColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crumb tracks crumbs styles.
|
// Crumb tracks crumbs styles.
|
||||||
Crumb struct {
|
Crumb struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
ActiveColor string `yaml:"activeColor"`
|
ActiveColor Color `yaml:"activeColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table tracks table styles.
|
// Table tracks table styles.
|
||||||
Table struct {
|
Table struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
CursorColor string `yaml:"cursorColor"`
|
CursorColor Color `yaml:"cursorColor"`
|
||||||
MarkColor string `yaml:"markColor"`
|
MarkColor Color `yaml:"markColor"`
|
||||||
Header TableHeader `yaml:"header"`
|
Header TableHeader `yaml:"header"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableHeader tracks table header styles.
|
// TableHeader tracks table header styles.
|
||||||
TableHeader struct {
|
TableHeader struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
SorterColor string `yaml:"sorterColor"`
|
SorterColor Color `yaml:"sorterColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xray tracks xray styles.
|
// Xray tracks xray styles.
|
||||||
Xray struct {
|
Xray struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor string `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
CursorColor string `yaml:"cursorColor"`
|
CursorColor Color `yaml:"cursorColor"`
|
||||||
GraphicColor string `yaml:"graphicColor"`
|
GraphicColor Color `yaml:"graphicColor"`
|
||||||
ShowIcons bool `yaml:"showIcons"`
|
ShowIcons bool `yaml:"showIcons"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu tracks menu styles.
|
// Menu tracks menu styles.
|
||||||
Menu struct {
|
Menu struct {
|
||||||
FgColor string `yaml:"fgColor"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
KeyColor string `yaml:"keyColor"`
|
KeyColor Color `yaml:"keyColor"`
|
||||||
NumKeyColor string `yaml:"numKeyColor"`
|
NumKeyColor Color `yaml:"numKeyColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style tracks K9s styles.
|
// Charts tracks charts styles.
|
||||||
Style struct {
|
Charts struct {
|
||||||
Body Body `yaml:"body"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
Frame Frame `yaml:"frame"`
|
DialBgColor Color `yaml:"dialBgColor"`
|
||||||
Info Info `yaml:"info"`
|
ChartBgColor Color `yaml:"chartBgColor"`
|
||||||
Table Table `yaml:"table"`
|
DefaultDialColors Colors `yaml:"defaultDialColors"`
|
||||||
Xray Xray `yaml:"xray"`
|
DefaultChartColors Colors `yaml:"defaultChartColors"`
|
||||||
Views Views `yaml:"views"`
|
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 {
|
func newStyle() Style {
|
||||||
return Style{
|
return Style{
|
||||||
Body: newBody(),
|
Body: newBody(),
|
||||||
Frame: newFrame(),
|
Frame: newFrame(),
|
||||||
Info: newInfo(),
|
Info: newInfo(),
|
||||||
Table: newTable(),
|
|
||||||
Views: newViews(),
|
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 {
|
func newViews() Views {
|
||||||
return Views{
|
return Views{
|
||||||
|
Table: newTable(),
|
||||||
|
Xray: newXray(),
|
||||||
|
Charts: newCharts(),
|
||||||
Yaml: newYaml(),
|
Yaml: newYaml(),
|
||||||
Log: newLog(),
|
Log: newLog(),
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +253,7 @@ func newStatus() Status {
|
||||||
ErrorColor: "orangered",
|
ErrorColor: "orangered",
|
||||||
HighlightColor: "aqua",
|
HighlightColor: "aqua",
|
||||||
KillColor: "mediumpurple",
|
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
|
// DefaultSkin loads the default skin
|
||||||
func (s *Styles) DefaultSkin() {
|
func (s *Styles) DefaultSkin() {
|
||||||
s.K9s = newStyle()
|
s.K9s = newStyle()
|
||||||
|
|
@ -299,12 +369,12 @@ func (s *Styles) DefaultSkin() {
|
||||||
|
|
||||||
// FgColor returns the foreground color.
|
// FgColor returns the foreground color.
|
||||||
func (s *Styles) FgColor() tcell.Color {
|
func (s *Styles) FgColor() tcell.Color {
|
||||||
return AsColor(s.Body().FgColor)
|
return s.Body().FgColor.Color()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BgColor returns the background color.
|
// BgColor returns the background color.
|
||||||
func (s *Styles) BgColor() tcell.Color {
|
func (s *Styles) BgColor() tcell.Color {
|
||||||
return AsColor(s.Body().BgColor)
|
return s.Body().BgColor.Color()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddListener registers a new listener.
|
// AddListener registers a new listener.
|
||||||
|
|
@ -353,14 +423,19 @@ func (s *Styles) Title() Title {
|
||||||
return s.Frame().Title
|
return s.Frame().Title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Charts returns charts styles.
|
||||||
|
func (s *Styles) Charts() Charts {
|
||||||
|
return s.K9s.Views.Charts
|
||||||
|
}
|
||||||
|
|
||||||
// Table returns table styles.
|
// Table returns table styles.
|
||||||
func (s *Styles) Table() Table {
|
func (s *Styles) Table() Table {
|
||||||
return s.K9s.Table
|
return s.K9s.Views.Table
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xray returns xray styles.
|
// Xray returns xray styles.
|
||||||
func (s *Styles) Xray() Xray {
|
func (s *Styles) Xray() Xray {
|
||||||
return s.K9s.Xray
|
return s.K9s.Views.Xray
|
||||||
}
|
}
|
||||||
|
|
||||||
// Views returns views styles.
|
// Views returns views styles.
|
||||||
|
|
@ -388,19 +463,7 @@ func (s *Styles) Update() {
|
||||||
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
||||||
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
||||||
tview.Styles.PrimaryTextColor = s.FgColor()
|
tview.Styles.PrimaryTextColor = s.FgColor()
|
||||||
tview.Styles.BorderColor = AsColor(s.K9s.Frame.Border.FgColor)
|
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
||||||
tview.Styles.FocusColor = AsColor(s.K9s.Frame.Border.FocusColor)
|
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
||||||
s.fireStylesChanged()
|
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"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAsColor(t *testing.T) {
|
func TestColor(t *testing.T) {
|
||||||
uu := map[string]tcell.Color{
|
uu := map[string]tcell.Color{
|
||||||
"blah": tcell.ColorDefault,
|
"blah": tcell.ColorDefault,
|
||||||
"blue": tcell.ColorBlue,
|
"blue": tcell.ColorBlue,
|
||||||
|
|
@ -20,7 +20,7 @@ func TestAsColor(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
c, u := k, uu[k]
|
c, u := k, uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
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"))
|
assert.Nil(t, s.Load("testdata/empty_skin.yml"))
|
||||||
s.Update()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "cadetblue", s.Body().FgColor)
|
assert.Equal(t, "cadetblue", s.Body().FgColor.String())
|
||||||
assert.Equal(t, "black", s.Body().BgColor)
|
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||||
assert.Equal(t, "black", s.Table().BgColor)
|
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||||
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
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"))
|
assert.Nil(t, s.Load("testdata/black_and_wtf.yml"))
|
||||||
s.Update()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "white", s.Body().FgColor)
|
assert.Equal(t, "white", s.Body().FgColor.String())
|
||||||
assert.Equal(t, "black", s.Body().BgColor)
|
assert.Equal(t, "black", s.Body().BgColor.String())
|
||||||
assert.Equal(t, "black", s.Table().BgColor)
|
assert.Equal(t, "black", s.Table().BgColor.String())
|
||||||
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
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 {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no context path for %q", c.gvr)
|
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)
|
po, err := c.fetchPod(fqn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))
|
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
|
||||||
for _, co := range po.Spec.InitContainers {
|
for _, co := range po.Spec.InitContainers {
|
||||||
res = append(res, makeContainerRes(co, po, pmx, true))
|
res = append(res, makeContainerRes(co, po, pmx, true))
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ type Deployment struct {
|
||||||
Resource
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy deployments.
|
||||||
|
func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
|
||||||
|
return dp.Status.Replicas == dp.Status.AvailableReplicas
|
||||||
|
}
|
||||||
|
|
||||||
// Scale a Deployment.
|
// Scale a Deployment.
|
||||||
func (d *Deployment) Scale(path string, replicas int32) error {
|
func (d *Deployment) Scale(path string, replicas int32) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ type DaemonSet struct {
|
||||||
Resource
|
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.
|
// Restart a DaemonSet rollout.
|
||||||
func (d *DaemonSet) Restart(path string) error {
|
func (d *DaemonSet) Restart(path string) error {
|
||||||
ds, err := d.GetInstance(path)
|
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,11 +34,15 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
log.Warn().Msgf("No label selector found in context")
|
log.Warn().Msgf("No label selector found in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
mx := client.NewMetricsServer(n.Client())
|
var (
|
||||||
nmx, err := mx.FetchNodesMetrics()
|
nmx *mv1beta1.NodeMetricsList
|
||||||
if err != nil {
|
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")
|
log.Warn().Err(err).Msgf("No node metrics")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nn, err := FetchNodes(n.Factory, labels)
|
nn, err := FetchNodes(n.Factory, labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,16 @@ type Pod struct {
|
||||||
Resource
|
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.
|
// Get returns a resource instance if found, else an error.
|
||||||
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
o, err := p.Resource.Get(ctx, path)
|
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)
|
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No Deal!
|
var pmx *mv1beta1.PodMetrics
|
||||||
mx := client.NewMetricsServer(p.Client())
|
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||||
pmx, err := mx.FetchPodMetrics(path)
|
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
|
||||||
if err != nil {
|
log.Warn().Err(err).Msgf("No pod metrics")
|
||||||
log.Warn().Err(err).Msgf("No pods metrics")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &render.PodWithMetrics{Raw: u, MX: pmx}, nil
|
return &render.PodWithMetrics{Raw: u, MX: pmx}, nil
|
||||||
|
|
@ -77,11 +87,12 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
return oo, err
|
return oo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mx := client.NewMetricsServer(p.Client())
|
var pmx *mv1beta1.PodMetricsList
|
||||||
pmx, err := mx.FetchPodsMetrics(ns)
|
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
||||||
if err != nil {
|
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
|
||||||
log.Warn().Err(err).Msgf("No pods metrics")
|
log.Warn().Err(err).Msgf("No pods metrics")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var res []runtime.Object
|
var res []runtime.Object
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
|
|
@ -128,7 +139,7 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cc := []string{}
|
cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
|
||||||
for _, c := range pod.Spec.Containers {
|
for _, c := range pod.Spec.Containers {
|
||||||
cc = append(cc, c.Name)
|
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) {
|
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{
|
m[client.NewGVR("xrays")] = metav1.APIResource{
|
||||||
Name: "xray",
|
Name: "xray",
|
||||||
Kind: "XRays",
|
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
|
Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsHappy check for happy sts.
|
||||||
|
func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
|
||||||
|
return sts.Status.Replicas == sts.Status.ReadyReplicas
|
||||||
|
}
|
||||||
|
|
||||||
// Scale a StatefulSet.
|
// Scale a StatefulSet.
|
||||||
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
||||||
ns, n := client.Namespaced(path)
|
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"
|
KeyApp ContextKey = "app"
|
||||||
KeyStyles ContextKey = "styles"
|
KeyStyles ContextKey = "styles"
|
||||||
KeyMetrics ContextKey = "metrics"
|
KeyMetrics ContextKey = "metrics"
|
||||||
|
KeyToast ContextKey = "toast"
|
||||||
|
KeyWithMetrics ContextKey = "withMetrics"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ type (
|
||||||
func NewCluster(f dao.Factory) *Cluster {
|
func NewCluster(f dao.Factory) *Cluster {
|
||||||
return &Cluster{
|
return &Cluster{
|
||||||
factory: f,
|
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{},
|
DAO: &dao.Chart{},
|
||||||
Renderer: &render.Chart{},
|
Renderer: &render.Chart{},
|
||||||
},
|
},
|
||||||
|
"pulses": {
|
||||||
|
DAO: &dao.Pulse{},
|
||||||
|
},
|
||||||
"openfaas": {
|
"openfaas": {
|
||||||
DAO: &dao.OpenFaas{},
|
DAO: &dao.OpenFaas{},
|
||||||
Renderer: &render.OpenFaas{},
|
Renderer: &render.OpenFaas{},
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ func (s *Stack) Pop() (Component, bool) {
|
||||||
var c Component
|
var c Component
|
||||||
s.mx.Lock()
|
s.mx.Lock()
|
||||||
{
|
{
|
||||||
c = s.components[s.size()]
|
c = s.components[len(s.components)-1]
|
||||||
s.components = s.components[:s.size()]
|
s.components = s.components[:len(s.components)-1]
|
||||||
}
|
}
|
||||||
s.mx.Unlock()
|
s.mx.Unlock()
|
||||||
s.notify(StackPop, c)
|
s.notify(StackPop, c)
|
||||||
|
|
@ -163,11 +163,7 @@ func (s *Stack) Top() Component {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.components[s.size()]
|
return s.components[len(s.components)-1]
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stack) size() int {
|
|
||||||
return len(s.components) - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stack) notify(a StackAction, c Component) {
|
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.
|
// ClusterWide checks if resource is scope for all namespaces.
|
||||||
func (t *Table) ClusterWide() bool {
|
func (t *Table) ClusterWide() bool {
|
||||||
|
log.Debug().Msgf("CLUSTER-WIDE %q", t.namespace)
|
||||||
return client.IsClusterWide(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) {
|
if client.IsClusterScoped(t.namespace) {
|
||||||
ns = client.AllNamespaces
|
ns = client.AllNamespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.List(ctx, ns)
|
return a.List(ctx, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ func TestTableReconcile(t *testing.T) {
|
||||||
f.rows = []runtime.Object{load(t, "p1")}
|
f.rows = []runtime.Object{load(t, "p1")}
|
||||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
err := ta.reconcile(ctx)
|
err := ta.reconcile(ctx)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
data := ta.Peek()
|
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, 1, len(data.RowEvents))
|
||||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +56,7 @@ func TestTableGet(t *testing.T) {
|
||||||
f := makeFactory()
|
f := makeFactory()
|
||||||
f.rows = []runtime.Object{load(t, "p1")}
|
f.rows = []runtime.Object{load(t, "p1")}
|
||||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
row, err := ta.Get(ctx, "fred")
|
row, err := ta.Get(ctx, "fred")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, row)
|
assert.NotNil(t, row)
|
||||||
|
|
@ -104,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
|
||||||
assert.Equal(t, 1, len(rr))
|
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) {
|
func TestTableGenericHydrate(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,10 @@ func TestTableRefresh(t *testing.T) {
|
||||||
f.rows = []runtime.Object{mustLoad("p1")}
|
f.rows = []runtime.Object{mustLoad("p1")}
|
||||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
ctx := context.WithValue(context.Background(), internal.KeyFactory, f)
|
||||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||||
|
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
|
||||||
ta.Refresh(ctx)
|
ta.Refresh(ctx)
|
||||||
data := ta.Peek()
|
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, 1, len(data.RowEvents))
|
||||||
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
assert.Equal(t, client.NamespaceAll, data.Namespace)
|
||||||
assert.Equal(t, 1, l.count)
|
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) {
|
if t.root == nil || t.root.Diff(root) {
|
||||||
t.root = root
|
t.root = root
|
||||||
t.fireTreeTreeChanged(t.root)
|
t.fireTreeChanged(t.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -251,7 +251,7 @@ func (t *Tree) resourceMeta() ResourceMeta {
|
||||||
return meta
|
return meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) fireTreeTreeChanged(root *xray.TreeNode) {
|
func (t *Tree) fireTreeChanged(root *xray.TreeNode) {
|
||||||
for _, l := range t.listeners {
|
for _, l := range t.listeners {
|
||||||
l.TreeChanged(root)
|
l.TreeChanged(root)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -8,6 +9,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
@ -28,11 +30,10 @@ var (
|
||||||
type Benchmark struct{}
|
type Benchmark struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Benchmark) ColorerFunc() ColorerFunc {
|
func (b Benchmark) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := tcell.ColorPaleGreen
|
c := tcell.ColorPaleGreen
|
||||||
statusCol := 2
|
if !Happy(ns, re.Row) {
|
||||||
if strings.TrimSpace(re.Row.Fields[statusCol]) != "pass" {
|
|
||||||
c = ErrColor
|
c = ErrColor
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
|
|
@ -50,6 +51,7 @@ func (Benchmark) Header(ns string) HeaderRow {
|
||||||
Header{Name: "2XX", Align: tview.AlignRight},
|
Header{Name: "2XX", Align: tview.AlignRight},
|
||||||
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
Header{Name: "4XX/5XX", Align: tview.AlignRight},
|
||||||
Header{Name: "REPORT"},
|
Header{Name: "REPORT"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +74,24 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.augmentRow(r.Fields, data)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +107,7 @@ func (Benchmark) readFile(file string) (string, error) {
|
||||||
return string(data), nil
|
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(), "_")
|
tokens := strings.Split(f.Name(), "_")
|
||||||
if len(tokens) < 2 {
|
if len(tokens) < 2 {
|
||||||
return fmt.Errorf("Invalid file name %s", f.Name())
|
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[0] = tokens[0]
|
||||||
row[1] = tokens[1]
|
row[1] = tokens[1]
|
||||||
row[7] = f.Name()
|
row[7] = f.Name()
|
||||||
row[8] = timeToAge(f.ModTime())
|
row[9] = timeToAge(f.ModTime())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ type Chart struct{}
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Chart) ColorerFunc() ColorerFunc {
|
func (Chart) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
return tcell.ColorMediumSpringGreen
|
return tcell.ColorMediumSpringGreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +39,7 @@ func (Chart) Header(ns string) HeaderRow {
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "CHART"},
|
Header{Name: "CHART"},
|
||||||
Header{Name: "APP VERSION"},
|
Header{Name: "APP VERSION"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.Info.Status.String(),
|
||||||
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
|
||||||
h.Release.Chart.Metadata.AppVersion,
|
h.Release.Chart.Metadata.AppVersion,
|
||||||
|
asStatus(c.diagnose(h.Release.Info.Status.String())),
|
||||||
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Chart) diagnose(s string) error {
|
||||||
|
if s != "deployed" {
|
||||||
|
return fmt.Errorf("chart is in an invalid state")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -36,18 +37,18 @@ type ContainerWithMetrics interface {
|
||||||
// Container renders a K8s Container to screen.
|
// Container renders a K8s Container to screen.
|
||||||
type Container struct{}
|
type Container struct{}
|
||||||
|
|
||||||
|
const readyCol = 2
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Container) ColorerFunc() ColorerFunc {
|
func (c Container) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
color := DefaultColorer(ns, re)
|
||||||
|
|
||||||
readyCol := 2
|
if !Happy(ns, re.Row) {
|
||||||
if strings.TrimSpace(r.Row.Fields[readyCol]) == "false" {
|
color = ErrColor
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCol := readyCol + 1
|
stateCol := readyCol + 1
|
||||||
switch strings.TrimSpace(r.Row.Fields[stateCol]) {
|
switch strings.TrimSpace(re.Row.Fields[stateCol]) {
|
||||||
case ContainerCreating, PodInitializing:
|
case ContainerCreating, PodInitializing:
|
||||||
return AddColor
|
return AddColor
|
||||||
case Terminating, Initialized:
|
case Terminating, Initialized:
|
||||||
|
|
@ -56,10 +57,10 @@ func (Container) ColorerFunc() ColorerFunc {
|
||||||
return CompletedColor
|
return CompletedColor
|
||||||
case Running:
|
case Running:
|
||||||
default:
|
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: "%CPU/L", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||||
Header{Name: "PORTS"},
|
Header{Name: "PORTS"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,12 +116,25 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
|
||||||
limit.cpu,
|
limit.cpu,
|
||||||
limit.mem,
|
limit.mem,
|
||||||
toStrPorts(co.Container.Ports),
|
toStrPorts(co.Container.Ports),
|
||||||
|
asStatus(c.diagnose(state, ready)),
|
||||||
toAge(co.Age),
|
toAge(co.Age),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ func TestContainer(t *testing.T) {
|
||||||
"50",
|
"50",
|
||||||
"20",
|
"20",
|
||||||
"",
|
"",
|
||||||
|
"container is not ready",
|
||||||
},
|
},
|
||||||
r.Fields[:len(r.Fields)-1],
|
r.Fields[:len(r.Fields)-1],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
|
||||||
func (ClusterRole) Header(string) HeaderRow {
|
func (ClusterRole) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.ID = client.FQN("-", cr.ObjectMeta.Name)
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
cr.Name,
|
cr.Name,
|
||||||
|
mapToStr(cr.Labels),
|
||||||
toAge(cr.ObjectMeta.CreationTimestamp),
|
toAge(cr.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ func (ClusterRoleBinding) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "CLUSTERROLE"},
|
Header{Name: "CLUSTERROLE"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "SUBJECT-KIND"},
|
||||||
Header{Name: "SUBJECTS"},
|
Header{Name: "SUBJECTS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +49,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
|
||||||
crb.RoleRef.Name,
|
crb.RoleRef.Name,
|
||||||
kind,
|
kind,
|
||||||
ss,
|
ss,
|
||||||
|
mapToStr(crb.Labels),
|
||||||
toAge(crb.ObjectMeta.CreationTimestamp),
|
toAge(crb.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestClusterRoleBindingRender(t *testing.T) {
|
||||||
c.Render(load(t, "crb"), "-", &r)
|
c.Render(load(t, "crb"), "-", &r)
|
||||||
|
|
||||||
assert.Equal(t, "-/blee", r.ID)
|
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 {
|
func (CustomResourceDefinition) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.ID = client.FQN(client.ClusterScope, extractMetaField(meta, "name"))
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
extractMetaField(meta, "name"),
|
extractMetaField(meta, "name"),
|
||||||
|
mapToIfc(meta["labels"]),
|
||||||
toAge(metav1.Time{Time: t}),
|
toAge(metav1.Time{Time: t}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +34,11 @@ func (CronJob) Header(ns string) HeaderRow {
|
||||||
Header{Name: "SUSPEND"},
|
Header{Name: "SUSPEND"},
|
||||||
Header{Name: "ACTIVE"},
|
Header{Name: "ACTIVE"},
|
||||||
Header{Name: "LAST_SCHEDULE"},
|
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},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -63,8 +71,64 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
|
||||||
boolPtrToStr(cj.Spec.Suspend),
|
boolPtrToStr(cj.Spec.Suspend),
|
||||||
strconv.Itoa(len(cj.Status.Active)),
|
strconv.Itoa(len(cj.Status.Active)),
|
||||||
lastScheduled,
|
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),
|
toAge(cj.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -17,19 +16,13 @@ import (
|
||||||
type Deployment struct{}
|
type Deployment struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Deployment) ColorerFunc() ColorerFunc {
|
func (d Deployment) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if !Happy(ns, r.Row) {
|
||||||
readyCol := 2
|
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
readyCol--
|
|
||||||
}
|
|
||||||
tokens := strings.Split(r.Row.Fields[readyCol], "/")
|
|
||||||
if tokens[0] != tokens[1] {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +42,9 @@ func (Deployment) Header(ns string) HeaderRow {
|
||||||
Header{Name: "READY"},
|
Header{Name: "READY"},
|
||||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", 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},
|
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,
|
r.Fields = append(r.Fields,
|
||||||
dp.Name,
|
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.UpdatedReplicas)),
|
||||||
strconv.Itoa(int(dp.Status.AvailableReplicas)),
|
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),
|
toAge(dp.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -17,18 +16,14 @@ import (
|
||||||
type DaemonSet struct{}
|
type DaemonSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (DaemonSet) ColorerFunc() ColorerFunc {
|
func (d DaemonSet) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
desiredCol := 2
|
if !Happy(ns, r.Row) {
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
desiredCol = 1
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[desiredCol]) != strings.TrimSpace(r.Row.Fields[desiredCol+2]) {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +45,8 @@ func (DaemonSet) Header(ns string) HeaderRow {
|
||||||
Header{Name: "READY", Align: tview.AlignRight},
|
Header{Name: "READY", Align: tview.AlignRight},
|
||||||
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.NumberReady)),
|
||||||
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
|
strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),
|
||||||
strconv.Itoa(int(ds.Status.NumberAvailable)),
|
strconv.Itoa(int(ds.Status.NumberAvailable)),
|
||||||
|
mapToStr(ds.Labels),
|
||||||
|
asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),
|
||||||
toAge(ds.ObjectMeta.CreationTimestamp),
|
toAge(ds.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -17,19 +18,20 @@ import (
|
||||||
type Event struct{}
|
type Event struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Event) ColorerFunc() ColorerFunc {
|
func (e Event) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
|
|
||||||
|
if !Happy(ns, r.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
markCol := 3
|
markCol := 3
|
||||||
if !client.IsAllNamespaces(ns) {
|
if !client.IsAllNamespaces(ns) {
|
||||||
markCol = 2
|
markCol = 2
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(r.Row.Fields[markCol]) {
|
if strings.TrimSpace(r.Row.Fields[markCol]) == "Killing" {
|
||||||
case "Failed":
|
return KillColor
|
||||||
c = ErrColor
|
|
||||||
case "Killing":
|
|
||||||
c = KillColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
@ -45,10 +47,12 @@ func (Event) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "TYPE"},
|
||||||
Header{Name: "REASON"},
|
Header{Name: "REASON"},
|
||||||
Header{Name: "SOURCE"},
|
Header{Name: "SOURCE"},
|
||||||
Header{Name: "COUNT", Align: tview.AlignRight},
|
Header{Name: "COUNT", Align: tview.AlignRight},
|
||||||
Header{Name: "MESSAGE"},
|
Header{Name: "MESSAGE", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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,
|
r.Fields = append(r.Fields,
|
||||||
asRef(ev.InvolvedObject),
|
asRef(ev.InvolvedObject),
|
||||||
|
ev.Type,
|
||||||
ev.Reason,
|
ev.Reason,
|
||||||
ev.Source.Component,
|
ev.Source.Component,
|
||||||
strconv.Itoa(int(ev.Count)),
|
strconv.Itoa(int(ev.Count)),
|
||||||
ev.Message,
|
ev.Message,
|
||||||
|
asStatus(e.diagnose(ev.Type)),
|
||||||
toAge(ev.LastTimestamp))
|
toAge(ev.LastTimestamp))
|
||||||
|
|
||||||
return nil
|
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 {
|
func asRef(r v1.ObjectReference) string {
|
||||||
return strings.ToLower(r.Kind) + ":" + r.Name
|
return strings.ToLower(r.Kind) + ":" + r.Name
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,17 @@ func TestEventRender(t *testing.T) {
|
||||||
c.Render(load(t, "ev"), "", &r)
|
c.Render(load(t, "ev"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
|
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
|
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.
|
// SetTable sets the tabular resource.
|
||||||
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
func (g *Generic) SetTable(t *metav1beta1.Table) {
|
||||||
g.table = t
|
g.table = t
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
"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
|
const megaByte = 1024 * 1024
|
||||||
|
|
||||||
// ToMB converts bytes to megabytes.
|
// ToMB converts bytes to megabytes.
|
||||||
|
|
@ -20,6 +26,13 @@ func ToMB(v int64) float64 {
|
||||||
return float64(v) / megaByte
|
return float64(v) / megaByte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func asStatus(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
func asSelector(s *metav1.LabelSelector) string {
|
func asSelector(s *metav1.LabelSelector) string {
|
||||||
sel, err := metav1.LabelSelectorAsSelector(s)
|
sel, err := metav1.LabelSelectorAsSelector(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -84,7 +97,7 @@ func join(a []string, sep string) string {
|
||||||
|
|
||||||
var buff strings.Builder
|
var buff strings.Builder
|
||||||
buff.Grow(n)
|
buff.Grow(n)
|
||||||
buff.WriteString(a[0])
|
buff.WriteString(b[0])
|
||||||
for _, s := range b[1:] {
|
for _, s := range b[1:] {
|
||||||
buff.WriteString(sep)
|
buff.WriteString(sep)
|
||||||
buff.WriteString(s)
|
buff.WriteString(s)
|
||||||
|
|
@ -163,7 +176,40 @@ func mapToStr(m map[string]string) (s string) {
|
||||||
for i, k := range kk {
|
for i, k := range kk {
|
||||||
s += k + "=" + m[k]
|
s += k + "=" + m[k]
|
||||||
if i < len(kk)-1 {
|
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 += " "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ func TestJoin(t *testing.T) {
|
||||||
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
"std": {[]string{"a", "b", "c"}, "a,b,c"},
|
||||||
"blank": {[]string{"", "", ""}, ""},
|
"blank": {[]string{"", "", ""}, ""},
|
||||||
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
"sparse": {[]string{"a", "", "c"}, "a,c"},
|
||||||
|
"withBlank": {[]string{"", "a", "c"}, "a,c"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
|
|
@ -304,7 +305,7 @@ func TestMapToStr(t *testing.T) {
|
||||||
i map[string]string
|
i map[string]string
|
||||||
e 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{}, ""},
|
{map[string]string{}, ""},
|
||||||
}
|
}
|
||||||
for _, u := range uu {
|
for _, u := range uu {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
|
||||||
Header{Name: "MINPODS", Align: tview.AlignRight},
|
Header{Name: "MINPODS", Align: tview.AlignRight},
|
||||||
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
Header{Name: "MAXPODS", Align: tview.AlignRight},
|
||||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.MinReplicas)),
|
||||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||||
|
"",
|
||||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
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.MinReplicas)),
|
||||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||||
|
"",
|
||||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
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.MinReplicas)),
|
||||||
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
strconv.Itoa(int(hpa.Spec.MaxReplicas)),
|
||||||
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
|
||||||
|
"",
|
||||||
toAge(hpa.ObjectMeta.CreationTimestamp),
|
toAge(hpa.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ func (Ingress) Header(ns string) HeaderRow {
|
||||||
Header{Name: "HOSTS"},
|
Header{Name: "HOSTS"},
|
||||||
Header{Name: "ADDRESS"},
|
Header{Name: "ADDRESS"},
|
||||||
Header{Name: "PORT"},
|
Header{Name: "PORT"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +58,7 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
|
||||||
toHosts(ing.Spec.Rules),
|
toHosts(ing.Spec.Rules),
|
||||||
toAddress(ing.Status.LoadBalancer),
|
toAddress(ing.Status.LoadBalancer),
|
||||||
toTLSPorts(ing.Spec.TLS),
|
toTLSPorts(ing.Spec.TLS),
|
||||||
|
"",
|
||||||
toAge(ing.ObjectMeta.CreationTimestamp),
|
toAge(ing.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
v1 "k8s.io/api/core/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/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
|
@ -33,8 +34,10 @@ func (Job) Header(ns string) HeaderRow {
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "COMPLETIONS"},
|
Header{Name: "COMPLETIONS"},
|
||||||
Header{Name: "DURATION"},
|
Header{Name: "DURATION"},
|
||||||
Header{Name: "CONTAINERS"},
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
Header{Name: "IMAGES"},
|
Header{Name: "CONTAINERS", Wide: true},
|
||||||
|
Header{Name: "IMAGES", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +53,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
ready := toCompletion(job.Spec, job.Status)
|
||||||
|
|
||||||
r.ID = client.MetaFQN(job.ObjectMeta)
|
r.ID = client.MetaFQN(job.ObjectMeta)
|
||||||
r.Fields = make(Fields, 0, len(j.Header(ns)))
|
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)
|
cc, ii := toContainers(job.Spec.Template.Spec)
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
job.Name,
|
job.Name,
|
||||||
toCompletion(job.Spec, job.Status),
|
ready,
|
||||||
toDuration(job.Status),
|
toDuration(job.Status),
|
||||||
|
jobSelector(job.Spec),
|
||||||
cc,
|
cc,
|
||||||
ii,
|
ii,
|
||||||
|
asStatus(j.diagnose(ready, job.Status.CompletionTime)),
|
||||||
toAge(job.ObjectMeta.CreationTimestamp),
|
toAge(job.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestJobRender(t *testing.T) {
|
||||||
c.Render(load(t, "job"), "", &r)
|
c.Render(load(t, "job"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/hello-1567179180", r.ID)
|
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
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
"github.com/gdamore/tcell"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -22,8 +25,16 @@ const (
|
||||||
type Node struct{}
|
type Node struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Node) ColorerFunc() ColorerFunc {
|
func (n Node) ColorerFunc() ColorerFunc {
|
||||||
return DefaultColorer
|
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.
|
// Header returns a header row.
|
||||||
|
|
@ -31,17 +42,19 @@ func (Node) Header(_ string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "ROLE"},
|
Header{Name: "ROLE", Wide: true},
|
||||||
Header{Name: "VERSION"},
|
Header{Name: "VERSION", Wide: true},
|
||||||
Header{Name: "KERNEL"},
|
Header{Name: "KERNEL", Wide: true},
|
||||||
Header{Name: "INTERNAL-IP"},
|
Header{Name: "INTERNAL-IP", Wide: true},
|
||||||
Header{Name: "EXTERNAL-IP"},
|
Header{Name: "EXTERNAL-IP", Wide: true},
|
||||||
Header{Name: "CPU", Align: tview.AlignRight},
|
Header{Name: "CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "MEM", Align: tview.AlignRight},
|
Header{Name: "MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "%CPU", Align: tview.AlignRight},
|
Header{Name: "%CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM", Align: tview.AlignRight},
|
Header{Name: "%MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "ACPU", Align: tview.AlignRight},
|
Header{Name: "ACPU", Align: tview.AlignRight},
|
||||||
Header{Name: "AMEM", Align: tview.AlignRight},
|
Header{Name: "AMEM", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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)
|
c, a, p := gatherNodeMX(&no, oo.MX)
|
||||||
|
|
||||||
sta := make([]string, 10)
|
statuses := make(sort.StringSlice, 10)
|
||||||
status(no.Status, no.Spec.Unschedulable, sta)
|
status(no.Status, no.Spec.Unschedulable, statuses)
|
||||||
ro := make([]string, 10)
|
sort.Sort(statuses)
|
||||||
nodeRoles(&no, ro)
|
roles := make(sort.StringSlice, 10)
|
||||||
|
nodeRoles(&no, roles)
|
||||||
|
sort.Sort(roles)
|
||||||
|
|
||||||
r.ID = client.FQN("", na)
|
r.ID = client.FQN("", na)
|
||||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
no.Name,
|
no.Name,
|
||||||
join(sta, ","),
|
join(statuses, ","),
|
||||||
join(ro, ","),
|
join(roles, ","),
|
||||||
no.Status.NodeInfo.KubeletVersion,
|
no.Status.NodeInfo.KubeletVersion,
|
||||||
no.Status.NodeInfo.KernelVersion,
|
no.Status.NodeInfo.KernelVersion,
|
||||||
iIP,
|
iIP,
|
||||||
|
|
@ -90,12 +105,27 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
||||||
p.mem,
|
p.mem,
|
||||||
a.cpu,
|
a.cpu,
|
||||||
a.mem,
|
a.mem,
|
||||||
|
mapToStr(no.Labels),
|
||||||
|
asStatus(n.diagnose(statuses)),
|
||||||
toAge(no.ObjectMeta.CreationTimestamp),
|
toAge(no.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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...
|
// Helpers...
|
||||||
|
|
||||||
|
|
@ -156,6 +186,9 @@ func nodeRoles(node *v1.Node, res []string) {
|
||||||
res[index] = v
|
res[index] = v
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
|
if index >= len(res) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if empty(res) {
|
if empty(res) {
|
||||||
|
|
|
||||||
|
|
@ -28,12 +28,14 @@ func (NetworkPolicy) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "ING-SELECTOR"},
|
Header{Name: "ING-SELECTOR", Wide: true},
|
||||||
Header{Name: "ING-PORTS"},
|
Header{Name: "ING-PORTS"},
|
||||||
Header{Name: "ING-BLOCK"},
|
Header{Name: "ING-BLOCK"},
|
||||||
Header{Name: "EGR-SELECTOR"},
|
Header{Name: "EGR-SELECTOR", Wide: true},
|
||||||
Header{Name: "EGR-PORTS"},
|
Header{Name: "EGR-PORTS"},
|
||||||
Header{Name: "EGR-BLOCK"},
|
Header{Name: "EGR-BLOCK"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +68,8 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
|
||||||
es,
|
es,
|
||||||
ep,
|
ep,
|
||||||
eb,
|
eb,
|
||||||
|
mapToStr(np.Labels),
|
||||||
|
"",
|
||||||
toAge(np.ObjectMeta.CreationTimestamp),
|
toAge(np.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ import (
|
||||||
type Namespace struct{}
|
type Namespace struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Namespace) ColorerFunc() ColorerFunc {
|
func (n Namespace) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, r RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, r)
|
||||||
if r.Kind == EventAdd {
|
if r.Kind == EventAdd {
|
||||||
|
|
@ -25,9 +26,8 @@ func (Namespace) ColorerFunc() ColorerFunc {
|
||||||
if r.Kind == EventUpdate {
|
if r.Kind == EventUpdate {
|
||||||
c = StdColor
|
c = StdColor
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(r.Row.Fields[1]) {
|
if !Happy(ns, r.Row) {
|
||||||
case "Inactive", Terminating:
|
return ErrColor
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
|
||||||
c = HighlightColor
|
c = HighlightColor
|
||||||
|
|
@ -42,12 +42,14 @@ func (Namespace) Header(string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "STATUS"},
|
Header{Name: "STATUS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// 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)
|
raw, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected Namespace, but got %T", o)
|
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{
|
r.Fields = Fields{
|
||||||
ns.Name,
|
ns.Name,
|
||||||
string(ns.Status.Phase),
|
string(ns.Status.Phase),
|
||||||
|
mapToStr(ns.Labels),
|
||||||
|
asStatus(n.diagnose(ns.Status.Phase)),
|
||||||
toAge(ns.ObjectMeta.CreationTimestamp),
|
toAge(ns.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -23,8 +24,12 @@ const (
|
||||||
type OpenFaas struct{}
|
type OpenFaas struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (OpenFaas) ColorerFunc() ColorerFunc {
|
func (o OpenFaas) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
return tcell.ColorPaleTurquoise
|
return tcell.ColorPaleTurquoise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +49,7 @@ func (OpenFaas) Header(ns string) HeaderRow {
|
||||||
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
|
||||||
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
Header{Name: "REPLICAS", Align: tview.AlignRight},
|
||||||
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
Header{Name: "AVAILABLE", Align: tview.AlignRight},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.InvocationCount)),
|
||||||
strconv.Itoa(int(fn.Function.Replicas)),
|
strconv.Itoa(int(fn.Function.Replicas)),
|
||||||
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
strconv.Itoa(int(fn.Function.AvailableReplicas)),
|
||||||
|
asStatus(f.diagnose(status)),
|
||||||
toAge(metav1.Time{Time: time.Now()}),
|
toAge(metav1.Time{Time: time.Now()}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (OpenFaas) diagnose(status string) error {
|
||||||
|
if status != "Ready" {
|
||||||
|
return errors.New("function not ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -18,24 +17,19 @@ import (
|
||||||
type PodDisruptionBudget struct{}
|
type PodDisruptionBudget struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
func (p PodDisruptionBudget) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
markCol := 5
|
if !Happy(ns, re.Row) {
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
markCol--
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return StdColor
|
return StdColor
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -53,6 +47,8 @@ func (PodDisruptionBudget) Header(ns string) HeaderRow {
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||||
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
Header{Name: "EXPECTED", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.CurrentHealthy)),
|
||||||
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
||||||
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
||||||
|
mapToStr(pdb.Labels),
|
||||||
|
asStatus(p.diagnose(pdb.Spec.MinAvailable.IntVal, pdb.Status.CurrentHealthy)),
|
||||||
toAge(pdb.ObjectMeta.CreationTimestamp),
|
toAge(pdb.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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...
|
// Helpers...
|
||||||
|
|
||||||
func numbToStr(n *intstr.IntOrString) string {
|
func numbToStr(n *intstr.IntOrString) string {
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,11 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, re)
|
c := DefaultColorer(ns, re)
|
||||||
|
|
||||||
readyCol := 2
|
statusCol := 4
|
||||||
if !client.IsAllNamespaces(ns) {
|
if !client.IsAllNamespaces(ns) {
|
||||||
readyCol--
|
statusCol--
|
||||||
}
|
}
|
||||||
statusCol := readyCol + 1
|
status := strings.TrimSpace(re.Row.Fields[statusCol])
|
||||||
|
|
||||||
ready, status := strings.TrimSpace(re.Row.Fields[readyCol]), strings.TrimSpace(re.Row.Fields[statusCol])
|
|
||||||
c = p.checkReadyCol(ready, status, c)
|
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case ContainerCreating, PodInitializing:
|
case ContainerCreating, PodInitializing:
|
||||||
c = AddColor
|
c = AddColor
|
||||||
|
|
@ -42,28 +38,19 @@ func (p Pod) ColorerFunc() ColorerFunc {
|
||||||
case Completed:
|
case Completed:
|
||||||
c = CompletedColor
|
c = CompletedColor
|
||||||
case Running:
|
case Running:
|
||||||
|
c = StdColor
|
||||||
case Terminating:
|
case Terminating:
|
||||||
c = KillColor
|
c = KillColor
|
||||||
default:
|
default:
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
c = ErrColor
|
c = ErrColor
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
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.
|
// Header returns a header row.
|
||||||
func (Pod) Header(ns string) HeaderRow {
|
func (Pod) Header(ns string) HeaderRow {
|
||||||
var h HeaderRow
|
var h HeaderRow
|
||||||
|
|
@ -74,17 +61,19 @@ func (Pod) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "READY"},
|
Header{Name: "READY"},
|
||||||
Header{Name: "STATUS"},
|
|
||||||
Header{Name: "RS", Align: tview.AlignRight},
|
Header{Name: "RS", Align: tview.AlignRight},
|
||||||
|
Header{Name: "STATUS"},
|
||||||
Header{Name: "CPU", Align: tview.AlignRight},
|
Header{Name: "CPU", Align: tview.AlignRight},
|
||||||
Header{Name: "MEM", Align: tview.AlignRight},
|
Header{Name: "MEM", Align: tview.AlignRight},
|
||||||
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
Header{Name: "%CPU/R", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
Header{Name: "%MEM/R", Align: tview.AlignRight},
|
||||||
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
Header{Name: "%CPU/L", Align: tview.AlignRight},
|
||||||
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
Header{Name: "%MEM/L", Align: tview.AlignRight},
|
||||||
Header{Name: "IP"},
|
Header{Name: "IP", Wide: true},
|
||||||
Header{Name: "NODE"},
|
Header{Name: "NODE", Wide: true},
|
||||||
Header{Name: "QOS"},
|
Header{Name: "QOS", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +94,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
ss := po.Status.ContainerStatuses
|
ss := po.Status.ContainerStatuses
|
||||||
cr, _, rc := p.Statuses(ss)
|
cr, _, rc := p.Statuses(ss)
|
||||||
c, perc := p.gatherPodMX(&po, pwm.MX)
|
c, perc := p.gatherPodMX(&po, pwm.MX)
|
||||||
|
phase := p.Phase(&po)
|
||||||
r.ID = client.MetaFQN(po.ObjectMeta)
|
r.ID = client.MetaFQN(po.ObjectMeta)
|
||||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||||
if client.IsAllNamespaces(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,
|
r.Fields = append(r.Fields,
|
||||||
po.ObjectMeta.Name,
|
po.ObjectMeta.Name,
|
||||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||||
p.Phase(&po),
|
|
||||||
strconv.Itoa(rc),
|
strconv.Itoa(rc),
|
||||||
|
phase,
|
||||||
c.cpu,
|
c.cpu,
|
||||||
c.mem,
|
c.mem,
|
||||||
perc.cpu,
|
perc.cpu,
|
||||||
|
|
@ -125,12 +114,25 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
na(po.Status.PodIP),
|
na(po.Status.PodIP),
|
||||||
na(po.Spec.NodeName),
|
na(po.Spec.NodeName),
|
||||||
p.mapQOS(po.Status.QOSClass),
|
p.mapQOS(po.Status.QOSClass),
|
||||||
|
mapToStr(po.Labels),
|
||||||
|
asStatus(p.diagnose(phase, cr, len(ss))),
|
||||||
toAge(po.ObjectMeta.CreationTimestamp),
|
toAge(po.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,12 @@ type (
|
||||||
|
|
||||||
func TestPodColorer(t *testing.T) {
|
func TestPodColorer(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Running"}}
|
nsRow = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Running"}}
|
||||||
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "Boom"}}
|
toastNS = render.Row{Fields: render.Fields{"blee", "fred", "1/1", "0", "Boom"}}
|
||||||
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "Boom"}}
|
notReadyNS = render.Row{Fields: render.Fields{"blee", "fred", "0/1", "0", "Boom"}}
|
||||||
row = render.Row{Fields: render.Fields{"fred", "1/1", "Running"}}
|
row = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Running"}}
|
||||||
toast = render.Row{Fields: render.Fields{"fred", "1/1", "Boom"}}
|
toast = render.Row{Fields: render.Fields{"fred", "1/1", "0", "Boom"}}
|
||||||
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "Boom"}}
|
notReady = render.Row{Fields: render.Fields{"fred", "0/1", "0", "Boom"}}
|
||||||
)
|
)
|
||||||
|
|
||||||
uu := colorerUCs{
|
uu := colorerUCs{
|
||||||
|
|
@ -70,7 +70,7 @@ func TestPodRender(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "default/nginx", r.ID)
|
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])
|
assert.Equal(t, e, r.Fields[:14])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ func TestPodInitRender(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "default/nginx", r.ID)
|
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])
|
assert.Equal(t, e, r.Fields[:14])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ func rbacVerbHeader() HeaderRow {
|
||||||
Header{Name: "PATCH "},
|
Header{Name: "PATCH "},
|
||||||
Header{Name: "UPDATE"},
|
Header{Name: "UPDATE"},
|
||||||
Header{Name: "DELETE"},
|
Header{Name: "DELETE"},
|
||||||
Header{Name: "DLIST "},
|
Header{Name: "DEL-LIST "},
|
||||||
Header{Name: "EXTRAS"},
|
Header{Name: "EXTRAS", Wide: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,10 @@ func (Policy) Header(ns string) HeaderRow {
|
||||||
Header{Name: "API GROUP"},
|
Header{Name: "API GROUP"},
|
||||||
Header{Name: "BINDING"},
|
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.
|
// 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.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, asVerbs(p.Verbs)...)
|
||||||
|
r.Fields = append(r.Fields, "")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,6 @@ func TestPolicyRender(t *testing.T) {
|
||||||
"[orangered::b] 𐄂 [::]",
|
"[orangered::b] 𐄂 [::]",
|
||||||
"[orangered::b] 𐄂 [::]",
|
"[orangered::b] 𐄂 [::]",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
}, r.Fields)
|
}, r.Fields)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ func TestPortForwardRender(t *testing.T) {
|
||||||
"http://0.0.0.0:p1/",
|
"http://0.0.0.0:p1/",
|
||||||
"1",
|
"1",
|
||||||
"1",
|
"1",
|
||||||
|
"",
|
||||||
"2m",
|
"2m",
|
||||||
}, r.Fields)
|
}, r.Fields)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ func (PortForward) Header(ns string) HeaderRow {
|
||||||
Header{Name: "URL"},
|
Header{Name: "URL"},
|
||||||
Header{Name: "C"},
|
Header{Name: "C"},
|
||||||
Header{Name: "N"},
|
Header{Name: "N"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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]),
|
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]),
|
||||||
asNum(pf.Config.C),
|
asNum(pf.Config.C),
|
||||||
asNum(pf.Config.N),
|
asNum(pf.Config.N),
|
||||||
|
"",
|
||||||
pf.Age(),
|
pf.Age(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,26 @@ import (
|
||||||
type PersistentVolume struct{}
|
type PersistentVolume struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PersistentVolume) ColorerFunc() ColorerFunc {
|
func (p PersistentVolume) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
status := strings.TrimSpace(r.Row.Fields[4])
|
if !Happy(ns, re.Row) {
|
||||||
switch status {
|
return ErrColor
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.TrimSpace(re.Row.Fields[4]) {
|
||||||
case "Bound":
|
case "Bound":
|
||||||
c = StdColor
|
c = StdColor
|
||||||
case "Available":
|
case "Available":
|
||||||
c = tcell.ColorYellow
|
c = tcell.ColorYellow
|
||||||
default:
|
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header rbw.
|
// Header returns a header rbw.
|
||||||
|
|
@ -49,6 +49,9 @@ func (PersistentVolume) Header(string) HeaderRow {
|
||||||
Header{Name: "CLAIM"},
|
Header{Name: "CLAIM"},
|
||||||
Header{Name: "STORAGECLASS"},
|
Header{Name: "STORAGECLASS"},
|
||||||
Header{Name: "REASON"},
|
Header{Name: "REASON"},
|
||||||
|
Header{Name: "VOLUMEMODE", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,12 +93,30 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
|
||||||
claim,
|
claim,
|
||||||
class,
|
class,
|
||||||
pv.Status.Reason,
|
pv.Status.Reason,
|
||||||
|
p.volumeMode(pv.Spec.VolumeMode),
|
||||||
|
mapToStr(pv.Labels),
|
||||||
|
asStatus(p.diagnose(string(phase))),
|
||||||
toAge(pv.ObjectMeta.CreationTimestamp),
|
toAge(pv.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
@ -15,19 +14,14 @@ import (
|
||||||
type PersistentVolumeClaim struct{}
|
type PersistentVolumeClaim struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
func (p PersistentVolumeClaim) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
markCol := 2
|
return ErrColor
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
markCol--
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[markCol]) != "Bound" {
|
|
||||||
c = ErrColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
|
|
@ -49,6 +43,8 @@ func (PersistentVolumeClaim) Header(ns string) HeaderRow {
|
||||||
Header{Name: "CAPACITY"},
|
Header{Name: "CAPACITY"},
|
||||||
Header{Name: "ACCESS MODES"},
|
Header{Name: "ACCESS MODES"},
|
||||||
Header{Name: "STORAGECLASS"},
|
Header{Name: "STORAGECLASS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -96,8 +92,17 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
|
||||||
capacity,
|
capacity,
|
||||||
accessModes,
|
accessModes,
|
||||||
class,
|
class,
|
||||||
|
mapToStr(pvc.Labels),
|
||||||
|
asStatus(p.diagnose(string(phase))),
|
||||||
toAge(pvc.ObjectMeta.CreationTimestamp),
|
toAge(pvc.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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"},
|
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.
|
// 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.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, asVerbs(p.Verbs)...)
|
||||||
|
r.Fields = append(r.Fields, "")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ func (Role) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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,
|
row.Fields = append(row.Fields,
|
||||||
ro.Name,
|
ro.Name,
|
||||||
|
mapToStr(ro.Labels),
|
||||||
|
"",
|
||||||
toAge(ro.ObjectMeta.CreationTimestamp),
|
toAge(ro.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ func (RoleBinding) Header(ns string) HeaderRow {
|
||||||
Header{Name: "ROLE"},
|
Header{Name: "ROLE"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "KIND"},
|
||||||
Header{Name: "SUBJECTS"},
|
Header{Name: "SUBJECTS"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +60,8 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
|
||||||
rb.RoleRef.Name,
|
rb.RoleRef.Name,
|
||||||
kind,
|
kind,
|
||||||
ss,
|
ss,
|
||||||
|
mapToStr(rb.Labels),
|
||||||
|
"",
|
||||||
toAge(rb.ObjectMeta.CreationTimestamp),
|
toAge(rb.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -87,11 +91,11 @@ func toSubjectAlias(s string) string {
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case rbacv1.UserKind:
|
case rbacv1.UserKind:
|
||||||
return "USR"
|
return "User"
|
||||||
case rbacv1.GroupKind:
|
case rbacv1.GroupKind:
|
||||||
return "GRP"
|
return "Group"
|
||||||
case rbacv1.ServiceAccountKind:
|
case rbacv1.ServiceAccountKind:
|
||||||
return "SA"
|
return "SvcAcct"
|
||||||
default:
|
default:
|
||||||
return strings.ToUpper(s)
|
return strings.ToUpper(s)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestRoleBindingRender(t *testing.T) {
|
||||||
c.Render(load(t, "rb"), "", &r)
|
c.Render(load(t, "rb"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/blee", r.ID)
|
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.
|
// 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 {
|
func toAgeDuration(dur string) string {
|
||||||
d, err := time.ParseDuration(dur)
|
d, err := time.ParseDuration(dur)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "n/a"
|
return dur
|
||||||
}
|
}
|
||||||
return duration.HumanDuration(d)
|
return duration.HumanDuration(d)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ type Header struct {
|
||||||
Name string
|
Name string
|
||||||
Align int
|
Align int
|
||||||
Decorator DecoratorFunc
|
Decorator DecoratorFunc
|
||||||
|
Hide bool
|
||||||
|
Wide bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone copies a header.
|
// Clone copies a header.
|
||||||
|
|
@ -56,13 +58,7 @@ func (hh HeaderRow) Columns() []string {
|
||||||
|
|
||||||
// HasAge returns true if table has an age column.
|
// HasAge returns true if table has an age column.
|
||||||
func (hh HeaderRow) HasAge() bool {
|
func (hh HeaderRow) HasAge() bool {
|
||||||
for _, r := range hh {
|
return hh.IndexOf(ageCol) != -1
|
||||||
if r.Name == ageCol {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AgeCol checks if given column index is the age column.
|
// 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
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
|
|
@ -17,24 +16,19 @@ import (
|
||||||
type ReplicaSet struct{}
|
type ReplicaSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (ReplicaSet) ColorerFunc() ColorerFunc {
|
func (r ReplicaSet) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
markCol := 2
|
if !Happy(ns, re.Row) {
|
||||||
if !client.IsAllNamespaces(ns) {
|
|
||||||
markCol--
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Row.Fields[markCol]) != strings.TrimSpace(r.Row.Fields[markCol+1]) {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
return StdColor
|
return StdColor
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
|
|
@ -49,6 +43,8 @@ func (ReplicaSet) Header(ns string) HeaderRow {
|
||||||
Header{Name: "DESIRED", Align: tview.AlignRight},
|
Header{Name: "DESIRED", Align: tview.AlignRight},
|
||||||
Header{Name: "CURRENT", Align: tview.AlignRight},
|
Header{Name: "CURRENT", Align: tview.AlignRight},
|
||||||
Header{Name: "READY", Align: tview.AlignRight},
|
Header{Name: "READY", Align: tview.AlignRight},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.Spec.Replicas)),
|
||||||
strconv.Itoa(int(rs.Status.Replicas)),
|
strconv.Itoa(int(rs.Status.Replicas)),
|
||||||
strconv.Itoa(int(rs.Status.ReadyReplicas)),
|
strconv.Itoa(int(rs.Status.ReadyReplicas)),
|
||||||
|
mapToStr(rs.Labels),
|
||||||
|
asStatus(s.diagnose(rs)),
|
||||||
toAge(rs.ObjectMeta.CreationTimestamp),
|
toAge(rs.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "SECRET"},
|
Header{Name: "SECRET"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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,
|
r.Fields = append(r.Fields,
|
||||||
sa.Name,
|
sa.Name,
|
||||||
strconv.Itoa(len(sa.Secrets)),
|
strconv.Itoa(len(sa.Secrets)),
|
||||||
|
mapToStr(sa.Labels),
|
||||||
|
"",
|
||||||
toAge(sa.ObjectMeta.CreationTimestamp),
|
toAge(sa.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ func (StorageClass) Header(ns string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "PROVISIONER"},
|
Header{Name: "PROVISIONER"},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
Header{Name: "AGE", Decorator: AgeDecorator},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +44,8 @@ func (StorageClass) Render(o interface{}, ns string, r *Row) error {
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
sc.Name,
|
sc.Name,
|
||||||
string(sc.Provisioner),
|
string(sc.Provisioner),
|
||||||
|
mapToStr(sc.Labels),
|
||||||
|
"",
|
||||||
toAge(sc.ObjectMeta.CreationTimestamp),
|
toAge(sc.ObjectMeta.CreationTimestamp),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ var AgeDecorator = func(a string) string {
|
||||||
func (ScreenDump) Header(ns string) HeaderRow {
|
func (ScreenDump) Header(ns string) HeaderRow {
|
||||||
return HeaderRow{
|
return HeaderRow{
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
|
Header{Name: "DIR"},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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.ID = filepath.Join(f.Dir, f.File.Name())
|
||||||
r.Fields = Fields{
|
r.Fields = Fields{
|
||||||
f.File.Name(),
|
f.File.Name(),
|
||||||
|
f.Dir,
|
||||||
|
"",
|
||||||
timeToAge(f.File.ModTime()),
|
timeToAge(f.File.ModTime()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ func TestScreenDumpRender(t *testing.T) {
|
||||||
assert.Equal(t, "fred/blee/bob", r.ID)
|
assert.Equal(t, "fred/blee/bob", r.ID)
|
||||||
assert.Equal(t, render.Fields{
|
assert.Equal(t, render.Fields{
|
||||||
"bob",
|
"bob",
|
||||||
|
"fred/blee",
|
||||||
|
"",
|
||||||
}, r.Fields[:len(r.Fields)-1])
|
}, r.Fields[:len(r.Fields)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
|
@ -16,20 +15,13 @@ import (
|
||||||
type StatefulSet struct{}
|
type StatefulSet struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (StatefulSet) ColorerFunc() ColorerFunc {
|
func (s StatefulSet) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, r RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
c := DefaultColorer(ns, r)
|
c := DefaultColorer(ns, re)
|
||||||
if r.Kind == EventAdd || r.Kind == EventUpdate {
|
if re.Kind == EventAdd || re.Kind == EventUpdate {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
if !Happy(ns, re.Row) {
|
||||||
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 {
|
|
||||||
return ErrColor
|
return ErrColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,8 +39,12 @@ func (StatefulSet) Header(ns string) HeaderRow {
|
||||||
return append(h,
|
return append(h,
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "READY"},
|
Header{Name: "READY"},
|
||||||
Header{Name: "SELECTOR"},
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
Header{Name: "SERVICE"},
|
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},
|
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,
|
r.Fields = append(r.Fields,
|
||||||
sts.Name,
|
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),
|
asSelector(sts.Spec.Selector),
|
||||||
na(sts.Spec.ServiceName),
|
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),
|
toAge(sts.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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.Nil(t, c.Render(load(t, "sts"), "", &r))
|
||||||
assert.Equal(t, "default/nginx-sts", r.ID)
|
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.
|
// Subject renders a rbac to screen.
|
||||||
type Subject struct{}
|
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.
|
// ColorerFunc colors a resource row.
|
||||||
func (Subject) ColorerFunc() ColorerFunc {
|
func (Subject) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, re RowEvent) tcell.Color {
|
return func(ns string, re RowEvent) tcell.Color {
|
||||||
|
|
@ -24,6 +29,7 @@ func (Subject) Header(ns string) HeaderRow {
|
||||||
Header{Name: "NAME"},
|
Header{Name: "NAME"},
|
||||||
Header{Name: "KIND"},
|
Header{Name: "KIND"},
|
||||||
Header{Name: "FIRST LOCATION"},
|
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.Name,
|
||||||
res.Kind,
|
res.Kind,
|
||||||
res.FirstLocation,
|
res.FirstLocation,
|
||||||
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,10 @@ func (Service) Header(ns string) HeaderRow {
|
||||||
Header{Name: "TYPE"},
|
Header{Name: "TYPE"},
|
||||||
Header{Name: "CLUSTER-IP"},
|
Header{Name: "CLUSTER-IP"},
|
||||||
Header{Name: "EXTERNAL-IP"},
|
Header{Name: "EXTERNAL-IP"},
|
||||||
Header{Name: "SELECTOR"},
|
Header{Name: "SELECTOR", Wide: true},
|
||||||
Header{Name: "PORTS"},
|
Header{Name: "PORTS", Wide: true},
|
||||||
|
Header{Name: "LABELS", Wide: true},
|
||||||
|
Header{Name: "VALID", Wide: true},
|
||||||
Header{Name: "AGE", Decorator: AgeDecorator},
|
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,
|
r.Fields = append(r.Fields,
|
||||||
svc.ObjectMeta.Name,
|
svc.ObjectMeta.Name,
|
||||||
string(svc.Spec.Type),
|
string(svc.Spec.Type),
|
||||||
svc.Spec.ClusterIP,
|
toIP(svc.Spec.ClusterIP),
|
||||||
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
|
||||||
mapToStr(svc.Spec.Selector),
|
mapToStr(svc.Spec.Selector),
|
||||||
toPorts(svc.Spec.Ports),
|
toPorts(svc.Spec.Ports),
|
||||||
|
mapToStr(svc.Labels),
|
||||||
|
asStatus(s.diagnose()),
|
||||||
toAge(svc.ObjectMeta.CreationTimestamp),
|
toAge(svc.ObjectMeta.CreationTimestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (Service) diagnose() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
func toIP(ip string) string {
|
||||||
|
if ip == "" || ip == "None" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
func getSvcExtIPS(svc *v1.Service) []string {
|
func getSvcExtIPS(svc *v1.Service) []string {
|
||||||
results := []string{}
|
results := []string{}
|
||||||
|
|
||||||
|
|
@ -116,7 +131,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string {
|
||||||
if svcType == v1.ServiceTypeLoadBalancer {
|
if svcType == v1.ServiceTypeLoadBalancer {
|
||||||
return "<pending>"
|
return "<pending>"
|
||||||
}
|
}
|
||||||
return MissingValue
|
return ""
|
||||||
}
|
}
|
||||||
sort.Strings(ips)
|
sort.Strings(ips)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ func TestServiceRender(t *testing.T) {
|
||||||
c.Render(load(t, "svc"), "", &r)
|
c.Render(load(t, "svc"), "", &r)
|
||||||
|
|
||||||
assert.Equal(t, "default/dictionary1", r.ID)
|
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 (
|
import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -14,6 +15,7 @@ type App struct {
|
||||||
Configurator
|
Configurator
|
||||||
|
|
||||||
Main *Pages
|
Main *Pages
|
||||||
|
flash *model.Flash
|
||||||
actions KeyActions
|
actions KeyActions
|
||||||
views map[string]tview.Primitive
|
views map[string]tview.Primitive
|
||||||
cmdBuff *CmdBuff
|
cmdBuff *CmdBuff
|
||||||
|
|
@ -25,6 +27,7 @@ func NewApp(context string) *App {
|
||||||
Application: tview.NewApplication(),
|
Application: tview.NewApplication(),
|
||||||
actions: make(KeyActions),
|
actions: make(KeyActions),
|
||||||
Main: NewPages(),
|
Main: NewPages(),
|
||||||
|
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||||
}
|
}
|
||||||
a.ReloadStyles(context)
|
a.ReloadStyles(context)
|
||||||
|
|
@ -33,7 +36,6 @@ func NewApp(context string) *App {
|
||||||
"menu": NewMenu(a.Styles),
|
"menu": NewMenu(a.Styles),
|
||||||
"logo": NewLogo(a.Styles),
|
"logo": NewLogo(a.Styles),
|
||||||
"cmd": NewCommand(a.Styles),
|
"cmd": NewCommand(a.Styles),
|
||||||
"flash": NewFlash(&a, "Initializing..."),
|
|
||||||
"crumbs": NewCrumbs(a.Styles),
|
"crumbs": NewCrumbs(a.Styles),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,11 +241,6 @@ func (a *App) Logo() *Logo {
|
||||||
return a.views["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.
|
// Cmd returns app cmd.
|
||||||
func (a *App) Cmd() *Command {
|
func (a *App) Cmd() *Command {
|
||||||
return a.views["cmd"].(*Command)
|
return a.views["cmd"].(*Command)
|
||||||
|
|
@ -254,6 +251,11 @@ func (a *App) Menu() *Menu {
|
||||||
return a.views["menu"].(*Menu)
|
return a.views["menu"].(*Menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flash returns a flash model.
|
||||||
|
func (a *App) Flash() *model.Flash {
|
||||||
|
return a.flash
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ func TestAppViews(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp("")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
vv := []string{"crumbs", "logo", "cmd", "flash", "menu"}
|
vv := []string{"crumbs", "logo", "cmd", "menu"}
|
||||||
for i := range vv {
|
for i := range vv {
|
||||||
v := vv[i]
|
v := vv[i]
|
||||||
t.Run(v, func(t *testing.T) {
|
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.Crumbs())
|
||||||
assert.NotNil(t, a.Flash())
|
|
||||||
assert.NotNil(t, a.Logo())
|
assert.NotNil(t, a.Logo())
|
||||||
assert.NotNil(t, a.Cmd())
|
assert.NotNil(t, a.Cmd())
|
||||||
assert.NotNil(t, a.Menu())
|
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))
|
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context))
|
||||||
if c.Styles == nil {
|
if c.Styles == nil {
|
||||||
c.Styles = config.NewStyles()
|
c.Styles = config.NewStyles()
|
||||||
|
} else {
|
||||||
|
c.Styles.Reset()
|
||||||
}
|
}
|
||||||
if err := c.Styles.Load(clusterSkins); err != nil {
|
if err := c.Styles.Load(clusterSkins); err != nil {
|
||||||
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
||||||
|
|
@ -102,10 +104,10 @@ func (c *Configurator) updateStyles(f string) {
|
||||||
}
|
}
|
||||||
c.Styles.Update()
|
c.Styles.Update()
|
||||||
|
|
||||||
render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor)
|
render.StdColor = c.Styles.Frame().Status.NewColor.Color()
|
||||||
render.AddColor = config.AsColor(c.Styles.Frame().Status.AddColor)
|
render.AddColor = c.Styles.Frame().Status.AddColor.Color()
|
||||||
render.ModColor = config.AsColor(c.Styles.Frame().Status.ModifyColor)
|
render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
|
||||||
render.ErrColor = config.AsColor(c.Styles.Frame().Status.ErrorColor)
|
render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()
|
||||||
render.HighlightColor = config.AsColor(c.Styles.Frame().Status.HighlightColor)
|
render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()
|
||||||
render.CompletedColor = config.AsColor(c.Styles.Frame().Status.CompletedColor)
|
render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,30 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FlashInfo represents an info message.
|
emoHappy = "😎"
|
||||||
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
|
|
||||||
|
|
||||||
emoDoh = "😗"
|
emoDoh = "😗"
|
||||||
emoRed = "😡"
|
emoRed = "😡"
|
||||||
emoDead = "💀"
|
|
||||||
emoHappy = "😎"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// Flash represents a flash message indicator.
|
||||||
// FlashLevel represents flash message severity.
|
type Flash struct {
|
||||||
FlashLevel int
|
|
||||||
|
|
||||||
// Flash represents a flash message indicator.
|
|
||||||
Flash struct {
|
|
||||||
*tview.TextView
|
*tview.TextView
|
||||||
|
|
||||||
cancel context.CancelFunc
|
|
||||||
app *App
|
app *App
|
||||||
flushNow bool
|
testMode bool
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// NewFlash returns a new flash view.
|
// NewFlash returns a new flash view.
|
||||||
func NewFlash(app *App, m string) *Flash {
|
func NewFlash(app *App) *Flash {
|
||||||
f := Flash{
|
f := Flash{
|
||||||
app: app,
|
app: app,
|
||||||
TextView: tview.NewTextView(),
|
TextView: tview.NewTextView(),
|
||||||
|
|
@ -54,15 +33,14 @@ func NewFlash(app *App, m string) *Flash {
|
||||||
f.SetTextColor(tcell.ColorAqua)
|
f.SetTextColor(tcell.ColorAqua)
|
||||||
f.SetTextAlign(tview.AlignLeft)
|
f.SetTextAlign(tview.AlignLeft)
|
||||||
f.SetBorderPadding(0, 0, 1, 1)
|
f.SetBorderPadding(0, 0, 1, 1)
|
||||||
f.SetText(m)
|
|
||||||
f.app.Styles.AddListener(&f)
|
f.app.Styles.AddListener(&f)
|
||||||
|
|
||||||
return &f
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMode for testing...
|
// SetTestMode for testing ONLY!
|
||||||
func (f *Flash) TestMode() {
|
func (f *Flash) SetTestMode(b bool) {
|
||||||
f.flushNow = true
|
f.testMode = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// StylesChanged notifies listener the skin changed.
|
// StylesChanged notifies listener the skin changed.
|
||||||
|
|
@ -71,101 +49,53 @@ func (f *Flash) StylesChanged(s *config.Styles) {
|
||||||
f.SetTextColor(s.FgColor())
|
f.SetTextColor(s.FgColor())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info displays an info flash message.
|
func (f *Flash) Watch(ctx context.Context, c model.FlashChan) {
|
||||||
func (f *Flash) Info(msg string) {
|
defer log.Debug().Msgf("Flash Canceled!")
|
||||||
log.Info().Msg(msg)
|
for {
|
||||||
f.SetMessage(FlashInfo, msg)
|
select {
|
||||||
}
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
// Infof displays a formatted info flash message.
|
case msg := <-c:
|
||||||
func (f *Flash) Infof(fmat string, args ...interface{}) {
|
f.SetMessage(msg)
|
||||||
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...))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMessage sets flash message and level.
|
// SetMessage sets flash message and level.
|
||||||
func (f *Flash) SetMessage(level FlashLevel, msg ...string) {
|
func (f *Flash) SetMessage(m model.LevelMessage) {
|
||||||
if f.cancel != nil {
|
fn := func() {
|
||||||
f.cancel()
|
if 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))
|
|
||||||
} else {
|
|
||||||
f.app.QueueUpdateDraw(func() {
|
|
||||||
f.SetTextColor(flashColor(level))
|
|
||||||
f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
f.Clear()
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
f.SetTextColor(flashColor(m.Level))
|
||||||
|
f.SetText(flashEmoji(m.Level) + " " + m.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.testMode {
|
||||||
|
fn()
|
||||||
|
} else {
|
||||||
|
f.app.QueueUpdateDraw(fn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func flashEmoji(l FlashLevel) string {
|
func flashEmoji(l model.FlashLevel) string {
|
||||||
switch l {
|
switch l {
|
||||||
case FlashWarn:
|
case model.FlashWarn:
|
||||||
return emoDoh
|
return emoDoh
|
||||||
case FlashErr:
|
case model.FlashErr:
|
||||||
return emoRed
|
return emoRed
|
||||||
case FlashFatal:
|
|
||||||
return emoDead
|
|
||||||
default:
|
default:
|
||||||
return emoHappy
|
return emoHappy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func flashColor(l FlashLevel) tcell.Color {
|
func flashColor(l model.FlashLevel) tcell.Color {
|
||||||
switch l {
|
switch l {
|
||||||
case FlashWarn:
|
case model.FlashWarn:
|
||||||
return tcell.ColorOrange
|
return tcell.ColorOrange
|
||||||
case FlashErr:
|
case model.FlashErr:
|
||||||
return tcell.ColorOrangeRed
|
return tcell.ColorOrangeRed
|
||||||
case FlashFatal:
|
|
||||||
return tcell.ColorFuchsia
|
|
||||||
default:
|
default:
|
||||||
return tcell.ColorNavajoWhite
|
return tcell.ColorNavajoWhite
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,40 @@
|
||||||
package ui_test
|
package ui_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFlashInfo(t *testing.T) {
|
func TestFlash(t *testing.T) {
|
||||||
f := newFlash()
|
const delay = 1 * time.Millisecond
|
||||||
f.Info("Blee")
|
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))
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
f.Infof("Blee %s", "duh")
|
defer cancel()
|
||||||
assert.Equal(t, "😎 Blee duh\n", f.GetText(false))
|
|
||||||
}
|
a := ui.NewApp("test")
|
||||||
|
f := ui.NewFlash(a)
|
||||||
func TestFlashWarn(t *testing.T) {
|
f.SetTestMode(true)
|
||||||
f := newFlash()
|
go f.Watch(ctx, a.Flash().Channel())
|
||||||
f.Warn("Blee")
|
|
||||||
|
for k := range uu {
|
||||||
assert.Equal(t, "😗 Blee\n", f.GetText(false))
|
u := uu[k]
|
||||||
f.Warnf("Blee %s", "duh")
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, "😗 Blee duh\n", f.GetText(false))
|
a.Flash().SetMessage(u.l, u.i)
|
||||||
}
|
time.Sleep(delay)
|
||||||
|
assert.Equal(t, u.e, 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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,30 +60,30 @@ func (l *Logo) Reset() {
|
||||||
|
|
||||||
// Err displays a log error state.
|
// Err displays a log error state.
|
||||||
func (l *Logo) Err(msg string) {
|
func (l *Logo) Err(msg string) {
|
||||||
l.update(msg, "red")
|
l.update(msg, config.NewColor("red"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn displays a log warning state.
|
// Warn displays a log warning state.
|
||||||
func (l *Logo) Warn(msg string) {
|
func (l *Logo) Warn(msg string) {
|
||||||
l.update(msg, "mediumvioletred")
|
l.update(msg, config.NewColor("mediumvioletred"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info displays a log info state.
|
// Info displays a log info state.
|
||||||
func (l *Logo) Info(msg string) {
|
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.refreshStatus(msg, c)
|
||||||
l.refreshLogo(c)
|
l.refreshLogo(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logo) refreshStatus(msg, c string) {
|
func (l *Logo) refreshStatus(msg string, c config.Color) {
|
||||||
l.status.SetBackgroundColor(config.AsColor(c))
|
l.status.SetBackgroundColor(c.Color())
|
||||||
l.status.SetText(fmt.Sprintf("[white::b]%s", msg))
|
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()
|
l.logo.Clear()
|
||||||
for i, s := range LogoSmall {
|
for i, s := range LogoSmall {
|
||||||
fmt.Fprintf(l.logo, "[%s::b]%s", c, s)
|
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 {
|
func formatNSMenu(i int, name string, styles config.Frame) string {
|
||||||
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor, 1)
|
fmat := strings.Replace(menuIndexFmt, "[key", "["+styles.Menu.NumKeyColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":", -1)
|
||||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1)
|
||||||
return fmt.Sprintf(fmat, i, name)
|
return fmt.Sprintf(fmat, i, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
|
func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string {
|
||||||
menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:d]%s "
|
menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:d]%s "
|
||||||
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1)
|
fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1)
|
fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1)
|
fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor.String()+":", -1)
|
||||||
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
|
return fmt.Sprintf(fmat, toMnemonic(h.Mnemonic), h.Description)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ type Table struct {
|
||||||
sortCol SortColumn
|
sortCol SortColumn
|
||||||
colorerFn render.ColorerFunc
|
colorerFn render.ColorerFunc
|
||||||
decorateFn DecorateFunc
|
decorateFn DecorateFunc
|
||||||
|
wide bool
|
||||||
|
toast bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTable returns a new table view.
|
// NewTable returns a new table view.
|
||||||
|
|
@ -65,6 +67,7 @@ func (t *Table) Init(ctx context.Context) {
|
||||||
t.SetSelectable(true, false)
|
t.SetSelectable(true, false)
|
||||||
t.SetSelectionChangedFunc(t.selectionChanged)
|
t.SetSelectionChangedFunc(t.selectionChanged)
|
||||||
t.SetInputCapture(t.keyboard)
|
t.SetInputCapture(t.keyboard)
|
||||||
|
t.SetBackgroundColor(tcell.ColorDefault)
|
||||||
|
|
||||||
t.styles = mustExtractSyles(ctx)
|
t.styles = mustExtractSyles(ctx)
|
||||||
t.StylesChanged(t.styles)
|
t.StylesChanged(t.styles)
|
||||||
|
|
@ -72,17 +75,35 @@ func (t *Table) Init(ctx context.Context) {
|
||||||
|
|
||||||
// StylesChanged notifies the skin changed.
|
// StylesChanged notifies the skin changed.
|
||||||
func (t *Table) StylesChanged(s *config.Styles) {
|
func (t *Table) StylesChanged(s *config.Styles) {
|
||||||
t.SetBackgroundColor(config.AsColor(s.Table().BgColor))
|
t.SetBackgroundColor(s.Table().BgColor.Color())
|
||||||
t.SetBorderColor(config.AsColor(s.Table().FgColor))
|
t.SetBorderColor(s.Table().FgColor.Color())
|
||||||
t.SetBorderFocusColor(config.AsColor(s.Frame().Border.FocusColor))
|
t.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())
|
||||||
t.SetSelectedStyle(
|
t.SetSelectedStyle(
|
||||||
tcell.ColorBlack,
|
tcell.ColorBlack,
|
||||||
config.AsColor(t.styles.Table().CursorColor),
|
t.styles.Table().CursorColor.Color(),
|
||||||
tcell.AttrBold,
|
tcell.AttrBold,
|
||||||
)
|
)
|
||||||
t.Refresh()
|
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.
|
// Actions returns active menu bindings.
|
||||||
func (t *Table) Actions() KeyActions {
|
func (t *Table) Actions() KeyActions {
|
||||||
return t.actions
|
return t.actions
|
||||||
|
|
@ -166,10 +187,7 @@ func (t *Table) Update(data render.TableData) {
|
||||||
if t.decorateFn != nil {
|
if t.decorateFn != nil {
|
||||||
data = t.decorateFn(data)
|
data = t.decorateFn(data)
|
||||||
}
|
}
|
||||||
if !t.cmdBuff.Empty() {
|
t.doUpdate(t.filtered(data))
|
||||||
data = t.filtered(data)
|
|
||||||
}
|
|
||||||
t.doUpdate(data)
|
|
||||||
t.UpdateTitle()
|
t.UpdateTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,13 +200,18 @@ func (t *Table) doUpdate(data render.TableData) {
|
||||||
|
|
||||||
t.Clear()
|
t.Clear()
|
||||||
t.adjustSorter(data)
|
t.adjustSorter(data)
|
||||||
fg := config.AsColor(t.styles.Table().Header.FgColor)
|
fg := t.styles.Table().Header.FgColor.Color()
|
||||||
bg := config.AsColor(t.styles.Table().Header.BgColor)
|
bg := t.styles.Table().Header.BgColor.Color()
|
||||||
for col, h := range data.Header {
|
var col int
|
||||||
|
for _, h := range data.Header {
|
||||||
|
if h.Wide && !t.wide {
|
||||||
|
continue
|
||||||
|
}
|
||||||
t.AddHeaderCell(col, h)
|
t.AddHeaderCell(col, h)
|
||||||
c := t.GetCell(0, col)
|
c := t.GetCell(0, col)
|
||||||
c.SetBackgroundColor(bg)
|
c.SetBackgroundColor(bg)
|
||||||
c.SetTextColor(fg)
|
c.SetTextColor(fg)
|
||||||
|
col++
|
||||||
}
|
}
|
||||||
data.RowEvents.Sort(data.Namespace, t.sortCol.index, t.sortCol.asc)
|
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
|
index = 0
|
||||||
case -1:
|
case -1:
|
||||||
index = t.GetColumnCount() - 1
|
index = t.GetColumnCount() - 1
|
||||||
|
case -3:
|
||||||
|
index = t.GetColumnCount() - 2
|
||||||
default:
|
default:
|
||||||
index = t.NameColIndex() + col
|
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
|
color = t.colorerFn
|
||||||
}
|
}
|
||||||
marked := t.IsMarked(re.Row.ID)
|
marked := t.IsMarked(re.Row.ID)
|
||||||
for col, field := range re.Row.Fields {
|
var col int
|
||||||
if !re.Deltas.IsBlank() && !header.AgeCol(col) {
|
for c, field := range re.Row.Fields {
|
||||||
field += Deltas(re.Deltas[col], field)
|
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 {
|
cell := tview.NewTableCell(field)
|
||||||
field = header[col].Decorator(field)
|
cell.SetExpansion(1)
|
||||||
}
|
cell.SetAlign(header[c].Align)
|
||||||
|
cell.SetTextColor(color(ns, re))
|
||||||
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))
|
|
||||||
if marked {
|
if marked {
|
||||||
c.SetTextColor(config.AsColor(t.styles.Table().MarkColor))
|
cell.SetTextColor(t.styles.Table().MarkColor.Color())
|
||||||
}
|
}
|
||||||
if col == 0 {
|
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.
|
// NameColIndex returns the index of the resource name column.
|
||||||
func (t *Table) NameColIndex() int {
|
func (t *Table) NameColIndex() int {
|
||||||
col := 0
|
col := 0
|
||||||
|
if client.IsClusterScoped(t.GetModel().GetNamespace()) {
|
||||||
|
return col
|
||||||
|
}
|
||||||
if t.GetModel().ClusterWide() {
|
if t.GetModel().ClusterWide() {
|
||||||
col++
|
col++
|
||||||
}
|
}
|
||||||
|
|
@ -313,20 +345,25 @@ func (t *Table) AddHeaderCell(col int, h render.Header) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) filtered(data render.TableData) render.TableData {
|
func (t *Table) filtered(data render.TableData) render.TableData {
|
||||||
if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) {
|
filtered := data
|
||||||
return data
|
if t.toast {
|
||||||
|
filtered = filterToast(data)
|
||||||
}
|
}
|
||||||
q := t.cmdBuff.String()
|
if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) {
|
||||||
if IsFuzzySelector(q) {
|
return filtered
|
||||||
return fuzzyFilter(q[2:], t.NameColIndex(), data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
|
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
|
||||||
t.cmdBuff.Clear()
|
t.cmdBuff.Clear()
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,15 +85,15 @@ func TrimLabelSelector(s string) string {
|
||||||
// SkinTitle decorates a title.
|
// SkinTitle decorates a title.
|
||||||
func SkinTitle(fmat string, style config.Frame) string {
|
func SkinTitle(fmat string, style config.Frame) string {
|
||||||
bgColor := style.Title.BgColor
|
bgColor := style.Title.BgColor
|
||||||
if bgColor == "default" {
|
if bgColor == config.DefaultColor {
|
||||||
bgColor = "-"
|
bgColor = config.TransparentColor
|
||||||
}
|
}
|
||||||
fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+bgColor, -1)
|
fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor.String()+":"+bgColor.String(), -1)
|
||||||
fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1)
|
fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1)
|
fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor, 1)
|
fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor, 1)
|
fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor.String(), 1)
|
||||||
fmat = strings.Replace(fmat, ":bg:", ":"+bgColor+":", -1)
|
fmat = strings.Replace(fmat, ":bg:", ":"+bgColor.String()+":", -1)
|
||||||
|
|
||||||
return fmat
|
return fmat
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +118,25 @@ func formatCell(field string, padding int) string {
|
||||||
return field
|
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) {
|
func rxFilter(q string, data render.TableData) (render.TableData, error) {
|
||||||
rx, err := regexp.Compile(`(?i)` + q)
|
rx, err := regexp.Compile(`(?i)` + q)
|
||||||
if err != nil {
|
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) ClusterWide() bool { return false }
|
||||||
func (t *testModel) GetNamespace() string { return "blee" }
|
func (t *testModel) GetNamespace() string { return "blee" }
|
||||||
func (t *testModel) SetNamespace(string) {}
|
func (t *testModel) SetNamespace(string) {}
|
||||||
|
func (t *testModel) ToggleToast() {}
|
||||||
func (t *testModel) AddListener(model.TableListener) {}
|
func (t *testModel) AddListener(model.TableListener) {}
|
||||||
func (t *testModel) Watch(context.Context) {}
|
func (t *testModel) Watch(context.Context) {}
|
||||||
func (t *testModel) Get(ctx context.Context, path string) (runtime.Object, error) {
|
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