checkpoint
parent
cf79622be0
commit
a96fa843b5
|
|
@ -23,7 +23,7 @@ Big thanks in full effect to you all, I am so humbled and honored by your kind a
|
|||
|
||||
### Dracula Skin
|
||||
|
||||
Since we're in the thank you phase, might as well lasso in `Josh Symmonds` for contributing the `Dracula` K9s skin that is now available in this repo under the skins directory. Here is a sneak peek of what K9s looks like under that skin. I am hopeful that like minded `graphically` inclined K9ers will contribute cool skins for this project for us to share/use in our Kubernetes clusters.
|
||||
Since we're in the thank you phase, might as well lasso in `Josh Symonds` for contributing the `Dracula` K9s skin that is now available in this repo under the skins directory. Here is a sneak peek of what K9s looks like under that skin. I am hopeful that like minded `graphically` inclined K9ers will contribute cool skins for this project for us to share/use in our Kubernetes clusters.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/skins/dracula.png"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ func loadConfiguration() *config.Config {
|
|||
|
||||
// Try to access server version if that fail. Connectivity issue?
|
||||
if _, err := k9sCfg.GetConnection().ServerVersion(); err != nil {
|
||||
log.Panic().Err(err).Msg("K9s can't connect to cluster")
|
||||
log.Panic().Msgf("K9s can't connect to cluster -- %s", err)
|
||||
}
|
||||
log.Info().Msg("✅ Kubernetes connectivity")
|
||||
if err := k9sCfg.Save(); err != nil {
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ func (a *APIClient) DialOrDie() kubernetes.Interface {
|
|||
|
||||
var err error
|
||||
if a.client, err = kubernetes.NewForConfig(a.RestConfigOrDie()); err != nil {
|
||||
log.Fatal().Msgf("Unable to connect to api server %v", err)
|
||||
log.Fatal().Err(err).Msgf("Unable to connect to api server")
|
||||
}
|
||||
return a.client
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ func (a *APIClient) DialOrDie() kubernetes.Interface {
|
|||
func (a *APIClient) RestConfigOrDie() *restclient.Config {
|
||||
cfg, err := a.config.RESTConfig()
|
||||
if err != nil {
|
||||
log.Panic().Msgf("Unable to connect to api server %v", err)
|
||||
log.Fatal().Err(err).Msgf("Unable to connect to api server")
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,17 @@ package client
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
clientcmd "k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
const dialTimeout = 5 * time.Second
|
||||
|
||||
// Config tracks a kubernetes configuration.
|
||||
type Config struct {
|
||||
flags *genericclioptions.ConfigFlags
|
||||
|
|
@ -38,17 +33,23 @@ func NewConfig(f *genericclioptions.ConfigFlags) *Config {
|
|||
}
|
||||
|
||||
// CheckConnectivity return true if api server is cool or false otherwise.
|
||||
// BOZO!! No super sure about this approach either??
|
||||
func (c *Config) CheckConnectivity() bool {
|
||||
address := strings.Replace(c.restConfig.Host, "https://", "", 1)
|
||||
rx := regexp.MustCompile(`\A.+:\d+`)
|
||||
if !rx.MatchString(address) {
|
||||
address += ":443"
|
||||
}
|
||||
|
||||
if _, err := net.DialTimeout("tcp", address, dialTimeout); err != nil {
|
||||
log.Error().Err(err).Msgf("DIAL TIMEDOUT!")
|
||||
cfg, err := c.RESTConfig()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("K9s can't connect to cluster (config)")
|
||||
return false
|
||||
}
|
||||
client, err := kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("K9s can't connect to cluster (client)")
|
||||
return false
|
||||
}
|
||||
if _, err := client.ServerVersion(); err != nil {
|
||||
log.Error().Err(err).Msgf("K9s can't connect to cluster (serverVersion)")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ func NewMetricsServer(c Connection) *MetricsServer {
|
|||
|
||||
// NodesMetrics retrieves metrics for a given set of nodes.
|
||||
func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) {
|
||||
if nodes == nil || metrics == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, no := range nodes.Items {
|
||||
mmx[no.Name] = NodeMetrics{
|
||||
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
|
||||
|
|
@ -29,7 +33,6 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
|
|||
TotalMEM: toMB(no.Status.Capacity.Memory().Value()),
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range metrics.Items {
|
||||
if mx, ok := mmx[c.Name]; ok {
|
||||
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
|
||||
|
|
@ -41,6 +44,9 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
|
|||
|
||||
// ClusterLoad retrieves all cluster nodes metrics.
|
||||
func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *ClusterMetrics) error {
|
||||
if nos == nil || nmx == nil {
|
||||
return fmt.Errorf("invalid node or node metrics lists")
|
||||
}
|
||||
nodeMetrics := make(NodesMetrics, len(nos.Items))
|
||||
for _, no := range nos.Items {
|
||||
nodeMetrics[no.Name] = NodeMetrics{
|
||||
|
|
@ -48,7 +54,6 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
|
||||
}
|
||||
}
|
||||
|
||||
for _, mx := range nmx.Items {
|
||||
if m, ok := nodeMetrics[mx.Name]; ok {
|
||||
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
|
||||
|
|
@ -71,15 +76,18 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
|||
|
||||
// FetchNodesMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||
mx := mv1beta1.NodeMetricsList{}
|
||||
var mx mv1beta1.NodeMetricsList
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
|
||||
auth, err := m.CanI("", "metrics.k8s.io/v1beta1/nodes", ListAccess)
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
}
|
||||
if !auth {
|
||||
return &mx, fmt.Errorf("user is not authorized to list node metrics")
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
if err != nil {
|
||||
|
|
@ -90,7 +98,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
|||
|
||||
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
||||
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
|
||||
mx := mv1beta1.PodMetricsList{}
|
||||
var mx mv1beta1.PodMetricsList
|
||||
if !m.HasMetrics() {
|
||||
return &mx, fmt.Errorf("No metrics-server detected on cluster")
|
||||
}
|
||||
|
|
@ -99,9 +107,12 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e
|
|||
}
|
||||
|
||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", ListAccess)
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
}
|
||||
if !auth {
|
||||
return &mx, fmt.Errorf("user is not authorized to list pods metrics")
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
if err != nil {
|
||||
|
|
@ -122,9 +133,12 @@ func (m *MetricsServer) FetchPodMetrics(ns, sel string) (*mv1beta1.PodMetrics, e
|
|||
ns = AllNamespaces
|
||||
}
|
||||
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", GetAccess)
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return &mx, err
|
||||
}
|
||||
if !auth {
|
||||
return &mx, fmt.Errorf("user is not authorized to list pod metrics")
|
||||
}
|
||||
|
||||
client, err := m.MXDial()
|
||||
if err != nil {
|
||||
|
|
@ -136,6 +150,10 @@ func (m *MetricsServer) FetchPodMetrics(ns, sel string) (*mv1beta1.PodMetrics, e
|
|||
|
||||
// PodsMetrics retrieves metrics for all pods in a given namespace.
|
||||
func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) {
|
||||
if pods == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Compute all pod's containers metrics.
|
||||
for _, p := range pods.Items {
|
||||
var mx PodMetrics
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package client
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
|
@ -12,27 +13,52 @@ import (
|
|||
)
|
||||
|
||||
func TestPodsMetrics(t *testing.T) {
|
||||
m := NewMetricsServer(nil)
|
||||
uu := map[string]struct {
|
||||
metrics *mv1beta1.PodMetricsList
|
||||
eSize int
|
||||
e client.PodsMetrics
|
||||
}{
|
||||
"dud": {
|
||||
eSize: 0,
|
||||
},
|
||||
|
||||
metrics := v1beta1.PodMetricsList{
|
||||
Items: []v1beta1.PodMetrics{
|
||||
*makeMxPod("p1", "1", "4Gi"),
|
||||
*makeMxPod("p2", "50m", "1Mi"),
|
||||
"ok": {
|
||||
metrics: &v1beta1.PodMetricsList{
|
||||
Items: []v1beta1.PodMetrics{
|
||||
*makeMxPod("p1", "1", "4Gi"),
|
||||
*makeMxPod("p2", "50m", "1Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 2,
|
||||
e: client.PodsMetrics{
|
||||
"default/p1": client.PodMetrics{
|
||||
CurrentCPU: int64(3000),
|
||||
CurrentMEM: float64(12288),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mmx := make(PodsMetrics)
|
||||
m.PodsMetrics(&metrics, mmx)
|
||||
assert.Equal(t, 2, len(mmx))
|
||||
m := client.NewMetricsServer(nil)
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
mmx := make(client.PodsMetrics)
|
||||
m.PodsMetrics(u.metrics, mmx)
|
||||
|
||||
mx, ok := mmx["default/p1"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, int64(3000), mx.CurrentCPU)
|
||||
assert.Equal(t, float64(12288), mx.CurrentMEM)
|
||||
assert.Equal(t, u.eSize, len(mmx))
|
||||
if u.eSize == 0 {
|
||||
return
|
||||
}
|
||||
mx, ok := mmx["default/p1"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, u.e["default/p1"], mx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPodsMetrics(b *testing.B) {
|
||||
m := NewMetricsServer(nil)
|
||||
m := client.NewMetricsServer(nil)
|
||||
|
||||
metrics := v1beta1.PodMetricsList{
|
||||
Items: []v1beta1.PodMetrics{
|
||||
|
|
@ -41,7 +67,7 @@ func BenchmarkPodsMetrics(b *testing.B) {
|
|||
*makeMxPod("p3", "50m", "1Mi"),
|
||||
},
|
||||
}
|
||||
mmx := make(PodsMetrics, 3)
|
||||
mmx := make(client.PodsMetrics, 3)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
|
@ -51,33 +77,78 @@ func BenchmarkPodsMetrics(b *testing.B) {
|
|||
}
|
||||
|
||||
func TestNodesMetrics(t *testing.T) {
|
||||
m := NewMetricsServer(nil)
|
||||
|
||||
nodes := v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
makeNode("n1", "32", "128Gi", "50m", "2Mi"),
|
||||
makeNode("n2", "8", "4Gi", "50m", "10Mi"),
|
||||
uu := map[string]struct {
|
||||
nodes *v1.NodeList
|
||||
metrics *mv1beta1.NodeMetricsList
|
||||
eSize int
|
||||
e client.NodesMetrics
|
||||
}{
|
||||
"duds": {
|
||||
eSize: 0,
|
||||
},
|
||||
"no_nodes": {
|
||||
metrics: &v1beta1.NodeMetricsList{
|
||||
Items: []v1beta1.NodeMetrics{
|
||||
*makeMxNode("n1", "10", "8Gi"),
|
||||
*makeMxNode("n2", "50m", "1Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 0,
|
||||
},
|
||||
"no_metrics": {
|
||||
nodes: &v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
makeNode("n1", "32", "128Gi", "50m", "2Mi"),
|
||||
makeNode("n2", "8", "4Gi", "50m", "10Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 0,
|
||||
},
|
||||
"ok": {
|
||||
nodes: &v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
makeNode("n1", "32", "128Gi", "50m", "2Mi"),
|
||||
makeNode("n2", "8", "4Gi", "50m", "10Mi"),
|
||||
},
|
||||
},
|
||||
metrics: &v1beta1.NodeMetricsList{
|
||||
Items: []v1beta1.NodeMetrics{
|
||||
*makeMxNode("n1", "10", "8Gi"),
|
||||
*makeMxNode("n2", "50m", "1Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 2,
|
||||
e: client.NodesMetrics{
|
||||
"n1": client.NodeMetrics{
|
||||
TotalCPU: int64(32000),
|
||||
TotalMEM: float64(131072),
|
||||
AvailCPU: int64(50),
|
||||
AvailMEM: float64(2),
|
||||
CurrentMetrics: client.CurrentMetrics{
|
||||
CurrentCPU: int64(10000),
|
||||
CurrentMEM: float64(8192),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
metrics := v1beta1.NodeMetricsList{
|
||||
Items: []v1beta1.NodeMetrics{
|
||||
*makeMxNode("n1", "10", "8Gi"),
|
||||
*makeMxNode("n2", "50m", "1Mi"),
|
||||
},
|
||||
}
|
||||
m := client.NewMetricsServer(nil)
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
mmx := make(client.NodesMetrics)
|
||||
m.NodesMetrics(u.nodes, u.metrics, mmx)
|
||||
|
||||
mmx := make(NodesMetrics)
|
||||
m.NodesMetrics(&nodes, &metrics, mmx)
|
||||
assert.Equal(t, 2, len(mmx))
|
||||
mx, ok := mmx["n1"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, int64(32000), mx.TotalCPU)
|
||||
assert.Equal(t, float64(131072), mx.TotalMEM)
|
||||
assert.Equal(t, int64(50), mx.AvailCPU)
|
||||
assert.Equal(t, float64(2), mx.AvailMEM)
|
||||
assert.Equal(t, int64(10000), mx.CurrentCPU)
|
||||
assert.Equal(t, float64(8192), mx.CurrentMEM)
|
||||
assert.Equal(t, u.eSize, len(mmx))
|
||||
if u.eSize == 0 {
|
||||
return
|
||||
}
|
||||
mx, ok := mmx["n1"]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, u.e["n1"], mx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNodesMetrics(b *testing.B) {
|
||||
|
|
@ -95,8 +166,8 @@ func BenchmarkNodesMetrics(b *testing.B) {
|
|||
},
|
||||
}
|
||||
|
||||
m := NewMetricsServer(nil)
|
||||
mmx := make(NodesMetrics)
|
||||
m := client.NewMetricsServer(nil)
|
||||
mmx := make(client.NodesMetrics)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
|
@ -106,26 +177,65 @@ func BenchmarkNodesMetrics(b *testing.B) {
|
|||
}
|
||||
|
||||
func TestClusterLoad(t *testing.T) {
|
||||
m := NewMetricsServer(nil)
|
||||
uu := map[string]struct {
|
||||
nodes *v1.NodeList
|
||||
metrics *mv1beta1.NodeMetricsList
|
||||
eSize int
|
||||
e client.ClusterMetrics
|
||||
}{
|
||||
"duds": {
|
||||
eSize: 0,
|
||||
},
|
||||
"no_nodes": {
|
||||
metrics: &v1beta1.NodeMetricsList{
|
||||
Items: []v1beta1.NodeMetrics{
|
||||
*makeMxNode("n1", "10", "8Gi"),
|
||||
*makeMxNode("n2", "50m", "1Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 0,
|
||||
},
|
||||
"no_metrics": {
|
||||
nodes: &v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
makeNode("n1", "32", "128Gi", "50m", "2Mi"),
|
||||
makeNode("n2", "8", "4Gi", "50m", "10Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 0,
|
||||
},
|
||||
"ok": {
|
||||
|
||||
nodes := v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
makeNode("n1", "100m", "4Mi", "50m", "2Mi"),
|
||||
makeNode("n2", "100m", "4Mi", "50m", "2Mi"),
|
||||
nodes: &v1.NodeList{
|
||||
Items: []v1.Node{
|
||||
makeNode("n1", "100m", "4Mi", "50m", "2Mi"),
|
||||
makeNode("n2", "100m", "4Mi", "50m", "2Mi"),
|
||||
},
|
||||
},
|
||||
metrics: &v1beta1.NodeMetricsList{
|
||||
Items: []v1beta1.NodeMetrics{
|
||||
*makeMxNode("n1", "50m", "1Mi"),
|
||||
*makeMxNode("n2", "50m", "1Mi"),
|
||||
},
|
||||
},
|
||||
eSize: 2,
|
||||
e: client.ClusterMetrics{
|
||||
PercCPU: 100.0,
|
||||
PercMEM: 50.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
metrics := mv1beta1.NodeMetricsList{
|
||||
Items: []mv1beta1.NodeMetrics{
|
||||
*makeMxNode("n1", "50m", "1Mi"),
|
||||
*makeMxNode("n2", "50m", "1Mi"),
|
||||
},
|
||||
}
|
||||
m := client.NewMetricsServer(nil)
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
var cmx client.ClusterMetrics
|
||||
m.ClusterLoad(u.nodes, u.metrics, &cmx)
|
||||
|
||||
var mx ClusterMetrics
|
||||
m.ClusterLoad(&nodes, &metrics, &mx)
|
||||
assert.Equal(t, 100.0, mx.PercCPU)
|
||||
assert.Equal(t, 50.0, mx.PercMEM)
|
||||
assert.Equal(t, u.e, cmx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkClusterLoad(b *testing.B) {
|
||||
|
|
@ -143,8 +253,8 @@ func BenchmarkClusterLoad(b *testing.B) {
|
|||
},
|
||||
}
|
||||
|
||||
m := NewMetricsServer(nil)
|
||||
var mx ClusterMetrics
|
||||
m := client.NewMetricsServer(nil)
|
||||
var mx client.ClusterMetrics
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for n := 0; n < b.N; n++ {
|
||||
|
|
|
|||
|
|
@ -73,17 +73,18 @@ type Connection interface {
|
|||
CurrentNamespaceName() (string, error)
|
||||
}
|
||||
|
||||
type currentMetrics struct {
|
||||
// CurrentMetrics tracks current cpu/mem.
|
||||
type CurrentMetrics struct {
|
||||
CurrentCPU int64
|
||||
CurrentMEM float64
|
||||
}
|
||||
|
||||
// PodMetrics represent an aggregation of all pod containers metrics.
|
||||
type PodMetrics currentMetrics
|
||||
type PodMetrics CurrentMetrics
|
||||
|
||||
// NodeMetrics describes raw node metrics.
|
||||
type NodeMetrics struct {
|
||||
currentMetrics
|
||||
CurrentMetrics
|
||||
AvailCPU int64
|
||||
AvailMEM float64
|
||||
TotalCPU int64
|
||||
|
|
|
|||
|
|
@ -117,6 +117,15 @@ type (
|
|||
SorterColor string `yaml:"sorterColor"`
|
||||
}
|
||||
|
||||
// Xray tracks xray styles.
|
||||
Xray struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
BgColor string `yaml:"bgColor"`
|
||||
CursorColor string `yaml:"cursorColor"`
|
||||
GraphicColor string `yaml:"graphicColor"`
|
||||
ShowIcons bool `yaml:"showIcons"`
|
||||
}
|
||||
|
||||
// Menu tracks menu styles.
|
||||
Menu struct {
|
||||
FgColor string `yaml:"fgColor"`
|
||||
|
|
@ -130,6 +139,7 @@ type (
|
|||
Frame Frame `yaml:"frame"`
|
||||
Info Info `yaml:"info"`
|
||||
Table Table `yaml:"table"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Views Views `yaml:"views"`
|
||||
}
|
||||
)
|
||||
|
|
@ -139,8 +149,9 @@ func newStyle() Style {
|
|||
Body: newBody(),
|
||||
Frame: newFrame(),
|
||||
Info: newInfo(),
|
||||
Table: newGetTable(),
|
||||
Table: newTable(),
|
||||
Views: newViews(),
|
||||
Xray: newXray(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,8 +228,19 @@ func newInfo() Info {
|
|||
}
|
||||
}
|
||||
|
||||
// NewXray returns a new xray style.
|
||||
func newXray() Xray {
|
||||
return Xray{
|
||||
FgColor: "aqua",
|
||||
BgColor: "black",
|
||||
CursorColor: "whitesmoke",
|
||||
GraphicColor: "floralwhite",
|
||||
ShowIcons: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTable returns a new table style.
|
||||
func newGetTable() Table {
|
||||
func newTable() Table {
|
||||
return Table{
|
||||
FgColor: "aqua",
|
||||
BgColor: "black",
|
||||
|
|
@ -327,10 +349,15 @@ func (s *Styles) Title() Title {
|
|||
}
|
||||
|
||||
// GetTable returns table styles.
|
||||
func (s *Styles) GetTable() Table {
|
||||
func (s *Styles) Table() Table {
|
||||
return s.K9s.Table
|
||||
}
|
||||
|
||||
// Xray returns xray styles.
|
||||
func (s *Styles) Xray() Xray {
|
||||
return s.K9s.Xray
|
||||
}
|
||||
|
||||
// Views returns views styles.
|
||||
func (s *Styles) Views() Views {
|
||||
return s.K9s.Views
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func TestSkinNone(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "cadetblue", s.Body().FgColor)
|
||||
assert.Equal(t, "black", s.Body().BgColor)
|
||||
assert.Equal(t, "black", s.GetTable().BgColor)
|
||||
assert.Equal(t, "black", s.Table().BgColor)
|
||||
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
||||
|
|
@ -45,7 +45,7 @@ func TestSkin(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "white", s.Body().FgColor)
|
||||
assert.Equal(t, "black", s.Body().BgColor)
|
||||
assert.Equal(t, "black", s.GetTable().BgColor)
|
||||
assert.Equal(t, "black", s.Table().BgColor)
|
||||
assert.Equal(t, tcell.ColorWhite, s.FgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, s.BgColor())
|
||||
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
|
|
@ -86,9 +85,12 @@ func (c *Container) TailLogs(ctx context.Context, logChan chan<- string, opts Lo
|
|||
func (c *Container) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
||||
ns, _ := client.Namespaced(path)
|
||||
auth, err := c.Client().CanI(ns, "v1/pods:log", client.GetAccess)
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to view pod logs")
|
||||
}
|
||||
|
||||
ns, n := client.Namespaced(path)
|
||||
return c.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts), nil
|
||||
|
|
@ -98,10 +100,6 @@ func (c *Container) Logs(path string, opts *v1.PodLogOptions) (*restclient.Reque
|
|||
// Helpers...
|
||||
|
||||
func makeContainerRes(co v1.Container, po *v1.Pod, pmx *mv1beta1.PodMetrics, isInit bool) render.ContainerRes {
|
||||
defer func(t time.Time) {
|
||||
log.Debug().Msgf("MAKE-CO %s -- %v", co.Name, time.Since(t))
|
||||
}(time.Now())
|
||||
|
||||
cmx, err := containerMetrics(co.Name, pmx)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("No container metrics found for %s::%s", po.Name, co.Name)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -23,9 +25,12 @@ type CronJob struct {
|
|||
func (c *CronJob) Run(path string) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := c.Client().CanI(ns, "batch/v1beta1/cronjobs", []string{client.GetVerb, client.CreateVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorize to run cronjobs")
|
||||
}
|
||||
|
||||
// BOZO!! Factory resource??
|
||||
cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{})
|
||||
|
|
|
|||
|
|
@ -32,9 +32,12 @@ type Deployment struct {
|
|||
func (d *Deployment) Scale(path string, replicas int32) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to scale a deployment")
|
||||
}
|
||||
|
||||
scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
|
|
@ -60,9 +63,12 @@ func (d *Deployment) Restart(path string) error {
|
|||
|
||||
ns, _ := client.Namespaced(path)
|
||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to restart a deployment")
|
||||
}
|
||||
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -45,9 +45,12 @@ func (d *DaemonSet) Restart(path string) error {
|
|||
}
|
||||
|
||||
auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", []string{client.PatchVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to restart a daemonset")
|
||||
}
|
||||
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -89,11 +89,15 @@ func (g *Generic) ToYAML(path string) (string, error) {
|
|||
|
||||
// Delete deletes a resource.
|
||||
func (g *Generic) Delete(path string, cascade, force bool) error {
|
||||
log.Debug().Msgf("DELETE %q -- %t:%t", path, cascade, force)
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := g.Client().CanI(ns, g.gvr.String(), []string{client.DeleteVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to delete %s", path)
|
||||
}
|
||||
|
||||
p := metav1.DeletePropagationOrphan
|
||||
if cascade {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package dao
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/derailed/k9s/internal"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
|
|
@ -65,9 +66,13 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
|
||||
// FetchNodes retrieves all nodes.
|
||||
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
|
||||
var list v1.NodeList
|
||||
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
|
||||
if !auth || err != nil {
|
||||
return nil, err
|
||||
if err != nil {
|
||||
return &list, err
|
||||
}
|
||||
if !auth {
|
||||
return &list, fmt.Errorf("user is not authorized to list nodes")
|
||||
}
|
||||
|
||||
return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{
|
||||
|
|
|
|||
|
|
@ -106,9 +106,12 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
|||
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
||||
ns, _ := client.Namespaced(path)
|
||||
auth, err := p.Client().CanI(ns, "v1/pods:log", []string{client.GetVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to view pod logs")
|
||||
}
|
||||
|
||||
ns, n := client.Namespaced(path)
|
||||
return p.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts), nil
|
||||
|
|
|
|||
|
|
@ -26,9 +26,13 @@ type PortForward struct {
|
|||
func (p *PortForward) Delete(path string, cascade, force bool) error {
|
||||
ns, _ := client.Namespaced(path)
|
||||
auth, err := p.Client().CanI(ns, "v1/pods:portforward", []string{client.DeleteVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to delete port forward %s", path)
|
||||
}
|
||||
|
||||
p.Factory.DeleteForwarder(path)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -93,9 +93,12 @@ func (p *PortForwarder) Start(path, co, address string, ports []string) (*portfo
|
|||
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := p.CanI(ns, "v1/pods", []string{client.GetVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to get pods")
|
||||
}
|
||||
pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -105,9 +108,12 @@ func (p *PortForwarder) Start(path, co, address string, ports []string) (*portfo
|
|||
}
|
||||
|
||||
auth, err = p.CanI(ns, "v1/pods:portforward", []string{client.UpdateVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !auth {
|
||||
return nil, fmt.Errorf("user is not authorized to update portforward")
|
||||
}
|
||||
|
||||
rcfg := p.RestConfigOrDie()
|
||||
rcfg.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,13 @@ type StatefulSet struct {
|
|||
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
||||
ns, n := client.Namespaced(path)
|
||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to scale statefulsets")
|
||||
}
|
||||
|
||||
scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -59,9 +63,13 @@ func (s *StatefulSet) Restart(path string) error {
|
|||
|
||||
ns, _ := client.Namespaced(path)
|
||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets", []string{client.PatchVerb})
|
||||
if !auth || err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !auth {
|
||||
return fmt.Errorf("user is not authorized to update statefulsets")
|
||||
}
|
||||
|
||||
update, err := polymorphichelpers.ObjectRestarterFn(&ds)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -159,7 +159,6 @@ func (l *Log) Append(line string) {
|
|||
l.initialized = false
|
||||
l.fireLogCleared()
|
||||
}
|
||||
log.Debug().Msgf("APPEND %s", line)
|
||||
if len(l.lines) < int(l.logOptions.Lines) {
|
||||
l.lines = append(l.lines, line)
|
||||
} else {
|
||||
|
|
@ -169,7 +168,6 @@ func (l *Log) Append(line string) {
|
|||
l.lastSent = 0
|
||||
}
|
||||
}
|
||||
log.Debug().Msgf("MODEL %d--%d", len(l.lines), l.lastSent)
|
||||
}
|
||||
|
||||
// Notify fires of notifications to the listeners.
|
||||
|
|
@ -264,7 +262,6 @@ func (l *Log) fireLogError(err error) {
|
|||
}
|
||||
|
||||
func (l *Log) fireLogChanged(lines []string) {
|
||||
log.Debug().Msgf("FIRE LOGS CHANGED %v", lines)
|
||||
for _, lis := range l.listeners {
|
||||
lis.LogChanged(lines)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,6 +289,7 @@ func makeC(n string) c {
|
|||
|
||||
func (c c) Name() string { return c.name }
|
||||
func (c c) Hints() model.MenuHints { return nil }
|
||||
func (c c) ExtraHints() map[string]string { return nil }
|
||||
func (c c) Draw(tcell.Screen) {}
|
||||
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
|
||||
func (c c) SetRect(int, int, int, int) {}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,11 @@ func (t *Table) SetNamespace(ns string) {
|
|||
t.data.Clear()
|
||||
}
|
||||
|
||||
// InNamespace checks if current namespace matches desired namespace.
|
||||
func (t *Table) InNamespace(ns string) bool {
|
||||
return len(t.data.RowEvents) > 0 && t.namespace == ns
|
||||
}
|
||||
|
||||
// SetRefreshRate sets model refresh duration.
|
||||
func (t *Table) SetRefreshRate(d time.Duration) {
|
||||
t.refreshRate = d
|
||||
|
|
@ -158,11 +163,6 @@ func (t *Table) ClusterWide() bool {
|
|||
return client.IsClusterWide(t.namespace)
|
||||
}
|
||||
|
||||
// InNamespace checks if current namespace matches desired namespace.
|
||||
func (t *Table) InNamespace(ns string) bool {
|
||||
return t.namespace == ns
|
||||
}
|
||||
|
||||
// Empty return true if no model data.
|
||||
func (t *Table) Empty() bool {
|
||||
return len(t.data.RowEvents) == 0
|
||||
|
|
@ -210,7 +210,11 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
|
|||
}
|
||||
a.Init(factory, client.NewGVR(t.gvr))
|
||||
|
||||
return a.List(ctx, client.CleanseNamespace(t.namespace))
|
||||
ns := client.CleanseNamespace(t.namespace)
|
||||
if client.IsClusterScoped(t.namespace) {
|
||||
ns = client.AllNamespaces
|
||||
}
|
||||
return a.List(ctx, ns)
|
||||
}
|
||||
|
||||
func (t *Table) reconcile(ctx context.Context) error {
|
||||
|
|
@ -230,19 +234,18 @@ func (t *Table) reconcile(ctx context.Context) error {
|
|||
}
|
||||
|
||||
var rows render.Rows
|
||||
ns := client.CleanseNamespace(t.namespace)
|
||||
if _, ok := meta.Renderer.(*render.Generic); ok {
|
||||
table, ok := oo[0].(*metav1beta1.Table)
|
||||
if !ok {
|
||||
return fmt.Errorf("expecting a meta table but got %T", oo[0])
|
||||
}
|
||||
rows = make(render.Rows, len(table.Rows))
|
||||
if err := genericHydrate(ns, table, rows, meta.Renderer); err != nil {
|
||||
if err := genericHydrate(t.namespace, table, rows, meta.Renderer); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
rows = make(render.Rows, len(oo))
|
||||
if err := hydrate(ns, oo, rows, meta.Renderer); err != nil {
|
||||
if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ type Igniter interface {
|
|||
type Hinter interface {
|
||||
// Hints returns a collection of menu hints.
|
||||
Hints() MenuHints
|
||||
|
||||
// ExtraHints returns additional hints.
|
||||
ExtraHints() map[string]string
|
||||
}
|
||||
|
||||
// Primitive represents a UI primitive.
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
|||
if !ok {
|
||||
return fmt.Errorf("expecting a TableRow but got %T", o)
|
||||
}
|
||||
_, nns, err := resourceNS(row.Object.Raw)
|
||||
nns, err := resourceNS(row.Object.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -92,26 +92,26 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func resourceNS(raw []byte) (bool, string, error) {
|
||||
func resourceNS(raw []byte) (string, error) {
|
||||
var obj map[string]interface{}
|
||||
err := json.Unmarshal(raw, &obj)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
return "", err
|
||||
}
|
||||
|
||||
meta, ok := obj["metadata"].(map[string]interface{})
|
||||
if !ok {
|
||||
return false, "", errors.New("no metadata found on generic resource")
|
||||
return "", errors.New("no metadata found on generic resource")
|
||||
}
|
||||
|
||||
ns, ok := meta["namespace"]
|
||||
if !ok {
|
||||
return true, "", nil
|
||||
return client.ClusterScope, nil
|
||||
}
|
||||
|
||||
nns, ok := ns.(string)
|
||||
if !ok {
|
||||
return false, "", fmt.Errorf("expecting namespace string type but got %T", ns)
|
||||
return "", fmt.Errorf("expecting namespace string type but got %T", ns)
|
||||
}
|
||||
return false, nns, nil
|
||||
return nns, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func TestGenericRender(t *testing.T) {
|
|||
"clusterWide": {
|
||||
ns: client.ClusterScope,
|
||||
table: makeNoNSGeneric(),
|
||||
eID: "c1",
|
||||
eID: "-/c1",
|
||||
eFields: render.Fields{"c1", "c2", "c3"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "A"},
|
||||
|
|
@ -67,7 +67,7 @@ func TestGenericRender(t *testing.T) {
|
|||
"age": {
|
||||
ns: client.ClusterScope,
|
||||
table: makeAgeGeneric(),
|
||||
eID: "c1",
|
||||
eID: "-/c1",
|
||||
eFields: render.Fields{"c1", "c2", "Age"},
|
||||
eHeader: render.HeaderRow{
|
||||
render.Header{Name: "A"},
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
}
|
||||
|
||||
ss := po.Status.ContainerStatuses
|
||||
cr, _, rc := p.statuses(ss)
|
||||
cr, _, rc := p.Statuses(ss)
|
||||
c, perc := p.gatherPodMX(&po, pwm.MX)
|
||||
|
||||
r.ID = client.MetaFQN(po.ObjectMeta)
|
||||
|
|
@ -112,7 +112,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
|||
r.Fields = append(r.Fields,
|
||||
po.ObjectMeta.Name,
|
||||
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
|
||||
p.phase(&po),
|
||||
p.Phase(&po),
|
||||
strconv.Itoa(rc),
|
||||
c.cpu,
|
||||
c.mem,
|
||||
|
|
@ -213,7 +213,8 @@ func (*Pod) mapQOS(class v1.PodQOSClass) string {
|
|||
}
|
||||
}
|
||||
|
||||
func (*Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
|
||||
// Status reports current pod container statuses.
|
||||
func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
|
||||
for _, c := range ss {
|
||||
if c.State.Terminated != nil {
|
||||
ct++
|
||||
|
|
@ -227,7 +228,8 @@ func (*Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
|
|||
return
|
||||
}
|
||||
|
||||
func (p *Pod) phase(po *v1.Pod) string {
|
||||
// Phase reports the given pod phase.
|
||||
func (p *Pod) Phase(po *v1.Pod) string {
|
||||
status := string(po.Status.Phase)
|
||||
if po.Status.Reason != "" {
|
||||
if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason {
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ type App struct {
|
|||
}
|
||||
|
||||
// NewApp returns a new app.
|
||||
func NewApp(cluster string) *App {
|
||||
func NewApp(context string) *App {
|
||||
a := App{
|
||||
Application: tview.NewApplication(),
|
||||
actions: make(KeyActions),
|
||||
Main: NewPages(),
|
||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
||||
}
|
||||
a.ReloadStyles(cluster)
|
||||
a.ReloadStyles(context)
|
||||
|
||||
a.views = map[string]tview.Primitive{
|
||||
"menu": NewMenu(a.Styles),
|
||||
|
|
@ -85,8 +85,8 @@ func (a *App) StylesChanged(s *config.Styles) {
|
|||
}
|
||||
|
||||
// ReloadStyles reloads skin file.
|
||||
func (a *App) ReloadStyles(cluster string) {
|
||||
a.RefreshStyles(cluster)
|
||||
func (a *App) ReloadStyles(context string) {
|
||||
a.RefreshStyles(context)
|
||||
}
|
||||
|
||||
// Conn returns an api server connection.
|
||||
|
|
|
|||
|
|
@ -81,15 +81,15 @@ func BenchConfig(cluster string) string {
|
|||
}
|
||||
|
||||
// RefreshStyles load for skin configuration changes.
|
||||
func (c *Configurator) RefreshStyles(cluster string) {
|
||||
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", cluster))
|
||||
func (c *Configurator) RefreshStyles(context string) {
|
||||
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context))
|
||||
if c.Styles == nil {
|
||||
c.Styles = config.NewStyles()
|
||||
}
|
||||
if err := c.Styles.Load(clusterSkins); err != nil {
|
||||
log.Info().Msgf("No cluster specific skin file found -- %s", clusterSkins)
|
||||
log.Info().Msgf("No context specific skin file found -- %s", clusterSkins)
|
||||
} else {
|
||||
log.Debug().Msgf("Found cluster skins %s", clusterSkins)
|
||||
log.Debug().Msgf("Found context skins %s", clusterSkins)
|
||||
c.updateStyles(clusterSkins)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ func makeComponent(n string) c {
|
|||
|
||||
func (c c) HasFocus() bool { return true }
|
||||
func (c c) Hints() model.MenuHints { return nil }
|
||||
func (c c) ExtraHints() map[string]string { return nil }
|
||||
func (c c) Name() string { return c.name }
|
||||
func (c c) Draw(tcell.Screen) {}
|
||||
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
|
||||
|
|
|
|||
|
|
@ -72,12 +72,12 @@ func (t *Table) Init(ctx context.Context) {
|
|||
|
||||
// StylesChanged notifies the skin changed.
|
||||
func (t *Table) StylesChanged(s *config.Styles) {
|
||||
t.SetBackgroundColor(config.AsColor(s.GetTable().BgColor))
|
||||
t.SetBorderColor(config.AsColor(s.GetTable().FgColor))
|
||||
t.SetBackgroundColor(config.AsColor(s.Table().BgColor))
|
||||
t.SetBorderColor(config.AsColor(s.Table().FgColor))
|
||||
t.SetBorderFocusColor(config.AsColor(s.Frame().Border.FocusColor))
|
||||
t.SetSelectedStyle(
|
||||
tcell.ColorBlack,
|
||||
config.AsColor(t.styles.GetTable().CursorColor),
|
||||
config.AsColor(t.styles.Table().CursorColor),
|
||||
tcell.AttrBold,
|
||||
)
|
||||
t.Refresh()
|
||||
|
|
@ -128,6 +128,11 @@ func (t *Table) Hints() model.MenuHints {
|
|||
return t.actions.Hints()
|
||||
}
|
||||
|
||||
// ExtraHints returns additional hints.
|
||||
func (t *Table) ExtraHints() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFilteredData fetch filtered tabular data.
|
||||
func (t *Table) GetFilteredData() render.TableData {
|
||||
return t.filtered(t.GetModel().Peek())
|
||||
|
|
@ -172,8 +177,8 @@ func (t *Table) doUpdate(data render.TableData) {
|
|||
|
||||
t.Clear()
|
||||
t.adjustSorter(data)
|
||||
fg := config.AsColor(t.styles.GetTable().Header.FgColor)
|
||||
bg := config.AsColor(t.styles.GetTable().Header.BgColor)
|
||||
fg := config.AsColor(t.styles.Table().Header.FgColor)
|
||||
bg := config.AsColor(t.styles.Table().Header.BgColor)
|
||||
for col, h := range data.Header {
|
||||
t.AddHeaderCell(col, h)
|
||||
c := t.GetCell(0, col)
|
||||
|
|
@ -258,7 +263,7 @@ func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.Hea
|
|||
c.SetAlign(header[col].Align)
|
||||
c.SetTextColor(color(ns, re))
|
||||
if marked {
|
||||
c.SetTextColor(config.AsColor(t.styles.GetTable().MarkColor))
|
||||
c.SetTextColor(config.AsColor(t.styles.Table().MarkColor))
|
||||
}
|
||||
if col == 0 {
|
||||
c.SetReference(re.Row.ID)
|
||||
|
|
@ -296,7 +301,7 @@ func (t *Table) NameColIndex() int {
|
|||
|
||||
// AddHeaderCell configures a table cell header.
|
||||
func (t *Table) AddHeaderCell(col int, h render.Header) {
|
||||
c := tview.NewTableCell(sortIndicator(t.sortCol, t.styles.GetTable(), col, h.Name))
|
||||
c := tview.NewTableCell(sortIndicator(t.sortCol, t.styles.Table(), col, h.Name))
|
||||
c.SetExpansion(1)
|
||||
c.SetAlign(h.Align)
|
||||
t.SetCell(0, col, c)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
"github.com/derailed/tview"
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
type KeyListenerFunc func()
|
||||
|
||||
// Tree represents a tree view.
|
||||
type Tree struct {
|
||||
*tview.TreeView
|
||||
|
||||
actions KeyActions
|
||||
selectedItem string
|
||||
cmdBuff *CmdBuff
|
||||
expandNodes bool
|
||||
Count int
|
||||
keyListener KeyListenerFunc
|
||||
}
|
||||
|
||||
// NewTree returns a new view.
|
||||
func NewTree() *Tree {
|
||||
return &Tree{
|
||||
TreeView: tview.NewTreeView(),
|
||||
expandNodes: true,
|
||||
actions: make(KeyActions),
|
||||
cmdBuff: NewCmdBuff('/', FilterBuff),
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the view
|
||||
func (t *Tree) Init(ctx context.Context) error {
|
||||
t.bindKeys()
|
||||
t.SetBorder(true)
|
||||
t.SetBorderAttributes(tcell.AttrBold)
|
||||
t.SetBorderPadding(0, 0, 1, 1)
|
||||
t.SetGraphics(true)
|
||||
t.SetGraphicsColor(tcell.ColorFloralWhite)
|
||||
t.SetInputCapture(t.keyboard)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSelectedItem sets the currently selected node.
|
||||
func (t *Tree) SetSelectedItem(s string) {
|
||||
t.selectedItem = s
|
||||
}
|
||||
|
||||
// GetSelectedItem returns the currently selected item or blank if none.
|
||||
func (t *Tree) GetSelectedItem() string {
|
||||
return t.selectedItem
|
||||
}
|
||||
|
||||
// ExpandNodes returns true if nodes are expanded or false otherwise.
|
||||
func (t *Tree) ExpandNodes() bool {
|
||||
return t.expandNodes
|
||||
}
|
||||
|
||||
// CmdBuff returns the filter command.
|
||||
func (t *Tree) CmdBuff() *CmdBuff {
|
||||
return t.cmdBuff
|
||||
}
|
||||
|
||||
// SetKeyListenerFn sets a key entered listener.
|
||||
func (t *Tree) SetKeyListenerFn(f KeyListenerFunc) {
|
||||
t.keyListener = f
|
||||
}
|
||||
|
||||
// Actions returns active menu bindings.
|
||||
func (t *Tree) Actions() KeyActions {
|
||||
return t.actions
|
||||
}
|
||||
|
||||
// Hints returns the view hints.
|
||||
func (t *Tree) Hints() model.MenuHints {
|
||||
return t.actions.Hints()
|
||||
}
|
||||
|
||||
// ExtraHints returns additional hints.
|
||||
func (t *Tree) ExtraHints() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tree) bindKeys() {
|
||||
t.Actions().Add(KeyActions{
|
||||
KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true),
|
||||
KeyX: NewKeyAction("Expand/Collapse All", t.toggleCollapseCmd, true),
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
if t.cmdBuff.IsActive() {
|
||||
t.cmdBuff.Add(evt.Rune())
|
||||
t.ClearSelection()
|
||||
if t.keyListener != nil {
|
||||
t.keyListener()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
key = mapKey(evt)
|
||||
}
|
||||
|
||||
if a, ok := t.actions[key]; ok {
|
||||
return a.Action(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
}
|
||||
|
||||
func (t *Tree) noopCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return evt
|
||||
}
|
||||
|
||||
func (t *Tree) toggleCollapseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
t.expandNodes = !t.expandNodes
|
||||
t.GetRoot().Walk(func(node, parent *tview.TreeNode) bool {
|
||||
if parent != nil {
|
||||
node.SetExpanded(t.expandNodes)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearSelection clears the currently selected node.
|
||||
func (t *Tree) ClearSelection() {
|
||||
t.selectedItem = ""
|
||||
t.SetCurrentNode(nil)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func mapKey(evt *tcell.EventKey) tcell.Key {
|
||||
key := tcell.Key(evt.Rune())
|
||||
if evt.Modifiers() == tcell.ModAlt {
|
||||
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ type App struct {
|
|||
// NewApp returns a K9s app instance.
|
||||
func NewApp(cfg *config.Config) *App {
|
||||
a := App{
|
||||
App: ui.NewApp(cfg.K9s.CurrentCluster),
|
||||
App: ui.NewApp(cfg.K9s.CurrentContext),
|
||||
Content: NewPageStack(),
|
||||
}
|
||||
a.Config = cfg
|
||||
|
|
@ -318,7 +318,6 @@ func (a *App) Status(l ui.FlashLevel, msg string) {
|
|||
func (a *App) ClearStatus(flash bool) {
|
||||
a.Logo().Reset()
|
||||
if flash {
|
||||
log.Debug().Msgf("FLASH CLEARED!!")
|
||||
a.Flash().Clear()
|
||||
}
|
||||
a.Draw()
|
||||
|
|
|
|||
|
|
@ -328,12 +328,12 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
// Helpers...
|
||||
|
||||
func (b *Browser) setNamespace(ns string) {
|
||||
ns = client.CleanseNamespace(ns)
|
||||
if b.GetModel().InNamespace(ns) {
|
||||
return
|
||||
}
|
||||
if !b.meta.Namespaced {
|
||||
b.GetModel().SetNamespace(client.ClusterScope)
|
||||
return
|
||||
ns = client.ClusterScope
|
||||
}
|
||||
b.GetModel().SetNamespace(client.CleanseNamespace(ns))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ func fetchResources(app *App) (*v1.NodeList, *mv1beta1.NodeMetricsList, error) {
|
|||
func (c *ClusterInfo) refreshMetrics(cluster *model.Cluster, row int) {
|
||||
nos, nmx, err := fetchResources(c.app)
|
||||
if err != nil {
|
||||
log.Warn().Msgf("NodeMetrics %#v", err)
|
||||
log.Warn().Err(err).Msgf("NodeMetrics failed")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,11 @@ func (d *Details) Hints() model.MenuHints {
|
|||
return d.actions.Hints()
|
||||
}
|
||||
|
||||
// ExtraHints returns additional hints.
|
||||
func (d *Details) ExtraHints() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Details) bindKeys() {
|
||||
d.actions.Set(ui.KeyActions{
|
||||
tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false),
|
||||
|
|
|
|||
|
|
@ -77,17 +77,40 @@ func (h *Help) computeMaxes(hh model.MenuHints) {
|
|||
h.maxKey += 2
|
||||
}
|
||||
|
||||
func (h *Help) computeExtraMaxes(ee map[string]string) {
|
||||
h.maxDesc = 0
|
||||
for k := range ee {
|
||||
if len(k) > h.maxDesc {
|
||||
h.maxDesc = len(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Help) build() {
|
||||
h.Clear()
|
||||
|
||||
sections := []string{"RESOURCE", "GENERAL", "NAVIGATION", "HELP"}
|
||||
|
||||
h.maxRows = len(h.showGeneral())
|
||||
ff := []HelpFunc{h.app.Content.Top().Hints, h.showGeneral, h.showNav, h.showHelp}
|
||||
ff := []HelpFunc{
|
||||
h.app.Content.Top().Hints,
|
||||
h.showGeneral,
|
||||
h.showNav,
|
||||
h.showHelp,
|
||||
}
|
||||
var col int
|
||||
for i, section := range []string{"RESOURCE", "GENERAL", "NAVIGATION", "HELP"} {
|
||||
extras := h.app.Content.Top().ExtraHints()
|
||||
for i, section := range sections {
|
||||
hh := ff[i]()
|
||||
sort.Sort(hh)
|
||||
h.computeMaxes(hh)
|
||||
if extras != nil {
|
||||
h.computeExtraMaxes(extras)
|
||||
}
|
||||
h.addSection(col, section, hh)
|
||||
if i == 0 && extras != nil {
|
||||
h.addExtras(extras, col, len(hh))
|
||||
}
|
||||
col += 2
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +120,20 @@ func (h *Help) build() {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Help) addExtras(extras map[string]string, col, size int) {
|
||||
kk := make([]string, 0, len(extras))
|
||||
for k := range extras {
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.StringSlice(kk).Sort()
|
||||
row := size + 1
|
||||
for _, k := range kk {
|
||||
h.SetCell(row, col, padCell(extras[k], h.maxKey))
|
||||
h.SetCell(row, col+1, padCell(k, h.maxDesc))
|
||||
row++
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Help) showHelp() model.MenuHints {
|
||||
return model.MenuHints{
|
||||
{
|
||||
|
|
@ -236,42 +273,28 @@ func (h *Help) addSection(c int, title string, hh model.MenuHints) {
|
|||
h.maxRows = len(hh)
|
||||
}
|
||||
row := 0
|
||||
cell := tview.NewTableCell(title)
|
||||
cell.SetTextColor(tcell.ColorGreen)
|
||||
cell.SetAttributes(tcell.AttrBold)
|
||||
cell.SetExpansion(1)
|
||||
cell.SetAlign(tview.AlignLeft)
|
||||
h.SetCell(row, c, cell)
|
||||
h.SetCell(row, c, titleCell(title))
|
||||
h.addSpacer(c + 1)
|
||||
row++
|
||||
|
||||
for _, hint := range hh {
|
||||
col := c
|
||||
cell := tview.NewTableCell(render.Pad(toMnemonic(hint.Mnemonic), h.maxKey))
|
||||
if _, err := strconv.Atoi(hint.Mnemonic); err != nil {
|
||||
cell.SetTextColor(tcell.ColorDodgerBlue)
|
||||
} else {
|
||||
cell.SetTextColor(tcell.ColorFuchsia)
|
||||
}
|
||||
cell.SetAttributes(tcell.AttrBold)
|
||||
h.SetCell(row, col, cell)
|
||||
h.SetCell(row, col, keyCell(hint.Mnemonic, h.maxKey))
|
||||
col++
|
||||
cell = tview.NewTableCell(render.Pad(hint.Description, h.maxDesc))
|
||||
cell.SetTextColor(tcell.ColorWhite)
|
||||
h.SetCell(row, col, cell)
|
||||
h.SetCell(row, col, infoCell(hint.Description, h.maxDesc))
|
||||
row++
|
||||
}
|
||||
|
||||
if len(hh) < h.maxRows {
|
||||
for i := h.maxRows - len(hh); i > 0; i-- {
|
||||
col := c
|
||||
cell := tview.NewTableCell(render.Pad("", h.maxKey))
|
||||
h.SetCell(row, col, cell)
|
||||
col++
|
||||
cell = tview.NewTableCell(render.Pad("", h.maxDesc))
|
||||
h.SetCell(row, col, cell)
|
||||
row++
|
||||
}
|
||||
if len(hh) >= h.maxRows {
|
||||
return
|
||||
}
|
||||
|
||||
for i := h.maxRows - len(hh); i > 0; i-- {
|
||||
col := c
|
||||
h.SetCell(row, col, padCell("", h.maxKey))
|
||||
col++
|
||||
h.SetCell(row, col, padCell("", h.maxDesc))
|
||||
row++
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,3 +320,36 @@ func keyConv(s string) string {
|
|||
|
||||
return strings.Replace(s, "alt", "opt", 1)
|
||||
}
|
||||
|
||||
func titleCell(title string) *tview.TableCell {
|
||||
c := tview.NewTableCell(title)
|
||||
c.SetTextColor(tcell.ColorGreen)
|
||||
c.SetAttributes(tcell.AttrBold)
|
||||
c.SetExpansion(1)
|
||||
c.SetAlign(tview.AlignLeft)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func keyCell(k string, width int) *tview.TableCell {
|
||||
c := padCell(toMnemonic(k), width)
|
||||
if _, err := strconv.Atoi(k); err != nil {
|
||||
c.SetTextColor(tcell.ColorDodgerBlue)
|
||||
} else {
|
||||
c.SetTextColor(tcell.ColorFuchsia)
|
||||
}
|
||||
c.SetAttributes(tcell.AttrBold)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func infoCell(info string, width int) *tview.TableCell {
|
||||
c := padCell(info, width)
|
||||
c.SetTextColor(tcell.ColorWhite)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func padCell(s string, width int) *tview.TableCell {
|
||||
return tview.NewTableCell(render.Pad(s, width))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ func (l *Log) Init(ctx context.Context) (err error) {
|
|||
|
||||
// LogCleared clears the logs.
|
||||
func (l *Log) LogCleared() {
|
||||
log.Debug().Msgf("LOG-CLEARED")
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.logs.Clear()
|
||||
l.logs.ScrollTo(0, 0)
|
||||
|
|
@ -110,7 +109,6 @@ func (l *Log) LogFailed(err error) {
|
|||
|
||||
// LogChanged updates the logs.
|
||||
func (l *Log) LogChanged(lines []string) {
|
||||
log.Debug().Msgf("LOG-CHANGED %d", len(lines))
|
||||
l.app.QueueUpdateDraw(func() {
|
||||
l.Flush(lines)
|
||||
})
|
||||
|
|
@ -141,6 +139,11 @@ func (l *Log) Hints() model.MenuHints {
|
|||
return l.logs.Actions().Hints()
|
||||
}
|
||||
|
||||
// ExtraHints returns additional hints.
|
||||
func (l *Log) ExtraHints() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start runs the component.
|
||||
func (l *Log) Start() {
|
||||
l.model.Start()
|
||||
|
|
@ -228,7 +231,6 @@ func (l *Log) Logs() *Details {
|
|||
|
||||
func (l *Log) write(lines string) {
|
||||
fmt.Fprintln(l.ansiWriter, tview.Escape(lines))
|
||||
log.Debug().Msgf("LOG LINES %d", l.logs.GetLineCount())
|
||||
}
|
||||
|
||||
// Flush write logs to viewer.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/ui"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// LogsExtender adds log actions to a given viewer.
|
||||
|
|
@ -54,7 +53,6 @@ func isResourcePath(p string) bool {
|
|||
}
|
||||
|
||||
func (l *LogsExtender) showLogs(path string, prev bool) {
|
||||
log.Debug().Msgf("SHOWING LOGS path %q", path)
|
||||
// Need to load and wait for pods
|
||||
ns, _ := client.Namespaced(path)
|
||||
_, err := l.App().factory.CanForResource(ns, "v1/pods", client.MonitorAccess)
|
||||
|
|
|
|||
|
|
@ -25,21 +25,21 @@ func NewPicker() *Picker {
|
|||
}
|
||||
|
||||
// Init initializes the view.
|
||||
func (v *Picker) Init(ctx context.Context) error {
|
||||
func (p *Picker) Init(ctx context.Context) error {
|
||||
app, err := extractApp(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true)
|
||||
p.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true)
|
||||
|
||||
v.SetBorder(true)
|
||||
v.SetMainTextColor(tcell.ColorWhite)
|
||||
v.ShowSecondaryText(false)
|
||||
v.SetShortcutColor(tcell.ColorAqua)
|
||||
v.SetSelectedBackgroundColor(tcell.ColorAqua)
|
||||
v.SetTitle(" [aqua::b]Containers Picker ")
|
||||
v.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a, ok := v.actions[evt.Key()]; ok {
|
||||
p.SetBorder(true)
|
||||
p.SetMainTextColor(tcell.ColorWhite)
|
||||
p.ShowSecondaryText(false)
|
||||
p.SetShortcutColor(tcell.ColorAqua)
|
||||
p.SetSelectedBackgroundColor(tcell.ColorAqua)
|
||||
p.SetTitle(" [aqua::b]Containers Picker ")
|
||||
p.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if a, ok := p.actions[evt.Key()]; ok {
|
||||
a.Action(evt)
|
||||
evt = nil
|
||||
}
|
||||
|
|
@ -50,22 +50,27 @@ func (v *Picker) Init(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Start starts the view.
|
||||
func (v *Picker) Start() {}
|
||||
func (p *Picker) Start() {}
|
||||
|
||||
// Stop stops the view.
|
||||
func (v *Picker) Stop() {}
|
||||
func (p *Picker) Stop() {}
|
||||
|
||||
// Name returns the component name.
|
||||
func (v *Picker) Name() string { return "picker" }
|
||||
func (p *Picker) Name() string { return "picker" }
|
||||
|
||||
// Hints returns the view hints.
|
||||
func (v *Picker) Hints() model.MenuHints {
|
||||
return v.actions.Hints()
|
||||
func (p *Picker) Hints() model.MenuHints {
|
||||
return p.actions.Hints()
|
||||
}
|
||||
|
||||
func (v *Picker) populate(ss []string) {
|
||||
v.Clear()
|
||||
// ExtraHints returns additional hints.
|
||||
func (p *Picker) ExtraHints() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Picker) populate(ss []string) {
|
||||
p.Clear()
|
||||
for i, s := range ss {
|
||||
v.AddItem(s, "Select a container", rune('a'+i), nil)
|
||||
p.AddItem(s, "Select a container", rune('a'+i), nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
p.GetTable().ShowDeleted()
|
||||
for _, res := range sels {
|
||||
p.App().Flash().Infof("Delete resource %s -- %s", p.GVR(), res)
|
||||
if err := nuker.Delete(res, true, false); err != nil {
|
||||
if err := nuker.Delete(res, true, true); err != nil {
|
||||
p.App().Flash().Errf("Delete failed with %s", err)
|
||||
} else {
|
||||
p.App().factory.DeleteForwarder(res)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package view
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -48,9 +47,8 @@ func (p *PortForward) portForwardContext(ctx context.Context) context.Context {
|
|||
|
||||
func (p *PortForward) bindKeys(aa ui.KeyActions) {
|
||||
aa.Add(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Benchmarks", p.showBenchCmd, true),
|
||||
ui.KeyB: ui.NewKeyAction("Bench", p.benchCmd, true),
|
||||
ui.KeyK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true),
|
||||
tcell.KeyEnter: ui.NewKeyAction("View Benchmarks", p.showBenchCmd, true),
|
||||
tcell.KeyCtrlB: ui.NewKeyAction("Bench Run/Stop", p.toggleBenchCmd, true),
|
||||
tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true),
|
||||
ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd(2, true), false),
|
||||
ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd(4, true), false),
|
||||
|
|
@ -65,25 +63,16 @@ func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if p.bench != nil {
|
||||
log.Debug().Msg(">>> Benchmark cancelFned!!")
|
||||
p.App().Status(ui.FlashErr, "Benchmark Camceled!")
|
||||
p.bench.Cancel()
|
||||
}
|
||||
p.App().ClearStatus(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
sel := p.GetTable().GetSelectedItem()
|
||||
if sel == "" {
|
||||
p.App().ClearStatus(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.bench != nil {
|
||||
p.App().Flash().Err(errors.New("Only one benchmark allowed at a time"))
|
||||
sel := p.GetTable().GetSelectedItem()
|
||||
if sel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ func TestPortForwardNew(t *testing.T) {
|
|||
|
||||
assert.Nil(t, pf.Init(makeCtx()))
|
||||
assert.Equal(t, "PortForwards", pf.Name())
|
||||
assert.Equal(t, 8, len(pf.Hints()))
|
||||
assert.Equal(t, 7, len(pf.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,8 @@ func NewService(gvr client.GVR) ResourceViewer {
|
|||
|
||||
func (s *Service) bindKeys(aa ui.KeyActions) {
|
||||
aa.Add(ui.KeyActions{
|
||||
ui.KeyB: ui.NewKeyAction("Bench", s.benchCmd, true),
|
||||
ui.KeyK: ui.NewKeyAction("Bench Stop", s.benchStopCmd, true),
|
||||
ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false),
|
||||
tcell.KeyCtrlB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true),
|
||||
ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -62,17 +61,6 @@ func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) {
|
|||
showPodsWithLabels(app, path, svc.Spec.Selector)
|
||||
}
|
||||
|
||||
func (s *Service) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if s.bench != nil {
|
||||
log.Debug().Msg(">>> Benchmark canceled!!")
|
||||
s.App().Status(ui.FlashErr, "Benchmark Canceled!")
|
||||
s.bench.Cancel()
|
||||
}
|
||||
s.App().ClearStatus(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) checkSvc(row int) error {
|
||||
svcType := trimCellRelative(s.GetTable(), row, 1)
|
||||
if svcType != "NodePort" && svcType != "LoadBalancer" {
|
||||
|
|
@ -103,7 +91,15 @@ func (s *Service) reloadBenchCfg() error {
|
|||
return s.App().Bench.Reload(path)
|
||||
}
|
||||
|
||||
func (s *Service) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if s.bench != nil {
|
||||
log.Debug().Msg(">>> Benchmark canceled!!")
|
||||
s.App().Status(ui.FlashErr, "Benchmark Canceled!")
|
||||
s.bench.Cancel()
|
||||
s.App().ClearStatus(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
sel := s.GetTable().GetSelectedItem()
|
||||
if sel == "" || s.bench != nil {
|
||||
return evt
|
||||
|
|
|
|||
|
|
@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) {
|
|||
|
||||
assert.Nil(t, s.Init(makeCtx()))
|
||||
assert.Equal(t, "Services", s.Name())
|
||||
assert.Equal(t, 8, len(s.Hints()))
|
||||
assert.Equal(t, 7, len(s.Hints()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,39 +27,34 @@ const xrayTitle = "Xray"
|
|||
|
||||
// Xray represents an xray tree view.
|
||||
type Xray struct {
|
||||
*tview.TreeView
|
||||
*ui.Tree
|
||||
|
||||
actions ui.KeyActions
|
||||
app *App
|
||||
gvr client.GVR
|
||||
selectedNode string
|
||||
model *model.Tree
|
||||
cancelFn context.CancelFunc
|
||||
cmdBuff *ui.CmdBuff
|
||||
expandNodes bool
|
||||
meta metav1.APIResource
|
||||
count int
|
||||
envFn EnvFunc
|
||||
app *App
|
||||
gvr client.GVR
|
||||
meta metav1.APIResource
|
||||
model *model.Tree
|
||||
cancelFn context.CancelFunc
|
||||
envFn EnvFunc
|
||||
}
|
||||
|
||||
var _ ResourceViewer = (*Xray)(nil)
|
||||
|
||||
// NewXray returns a new view.
|
||||
func NewXray(gvr client.GVR) ResourceViewer {
|
||||
a := Xray{
|
||||
gvr: gvr,
|
||||
TreeView: tview.NewTreeView(),
|
||||
model: model.NewTree(gvr.String()),
|
||||
expandNodes: true,
|
||||
actions: make(ui.KeyActions),
|
||||
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
|
||||
return &Xray{
|
||||
gvr: gvr,
|
||||
Tree: ui.NewTree(),
|
||||
model: model.NewTree(gvr.String()),
|
||||
}
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
// Init initializes the view
|
||||
func (x *Xray) Init(ctx context.Context) error {
|
||||
if err := x.Tree.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
x.SetKeyListenerFn(x.keyEntered)
|
||||
|
||||
var err error
|
||||
x.meta, err = dao.MetaFor(x.gvr)
|
||||
if err != nil {
|
||||
|
|
@ -71,16 +66,11 @@ func (x *Xray) Init(ctx context.Context) error {
|
|||
}
|
||||
|
||||
x.bindKeys()
|
||||
x.SetBorder(true)
|
||||
x.SetBorderAttributes(tcell.AttrBold)
|
||||
x.SetBorderPadding(0, 0, 1, 1)
|
||||
x.SetBackgroundColor(config.AsColor(x.app.Styles.GetTable().BgColor))
|
||||
x.SetBorderColor(config.AsColor(x.app.Styles.GetTable().FgColor))
|
||||
x.SetBackgroundColor(config.AsColor(x.app.Styles.Xray().BgColor))
|
||||
x.SetBorderColor(config.AsColor(x.app.Styles.Xray().FgColor))
|
||||
x.SetBorderFocusColor(config.AsColor(x.app.Styles.Frame().Border.FocusColor))
|
||||
x.SetGraphicsColor(config.AsColor(x.app.Styles.Xray().GraphicColor))
|
||||
x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.R())))
|
||||
x.SetGraphics(true)
|
||||
x.SetGraphicsColor(tcell.ColorFloralWhite)
|
||||
x.SetInputCapture(x.keyboard)
|
||||
|
||||
x.model.SetRefreshRate(time.Duration(x.app.Config.K9s.GetRefreshRate()) * time.Second)
|
||||
x.model.SetNamespace(client.CleanseNamespace(x.app.Config.ActiveNamespace()))
|
||||
|
|
@ -92,7 +82,7 @@ func (x *Xray) Init(ctx context.Context) error {
|
|||
log.Error().Msgf("No ref found on node %s", n.GetText())
|
||||
return
|
||||
}
|
||||
x.selectedNode = ref.Path
|
||||
x.SetSelectedItem(ref.Path)
|
||||
x.refreshActions()
|
||||
})
|
||||
x.refreshActions()
|
||||
|
|
@ -100,24 +90,20 @@ func (x *Xray) Init(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ExtraHints returns additional hints.
|
||||
func (x *Xray) ExtraHints() map[string]string {
|
||||
if !x.app.Styles.Xray().ShowIcons {
|
||||
return nil
|
||||
}
|
||||
return xray.EmojiInfo()
|
||||
}
|
||||
|
||||
// SetInstance sets specific resource instance.
|
||||
func (x *Xray) SetInstance(string) {}
|
||||
|
||||
// Actions returns active menu bindings.
|
||||
func (x *Xray) Actions() ui.KeyActions {
|
||||
return x.actions
|
||||
}
|
||||
|
||||
// Hints returns the view hints.
|
||||
func (x *Xray) Hints() model.MenuHints {
|
||||
return x.actions.Hints()
|
||||
}
|
||||
|
||||
func (x *Xray) bindKeys() {
|
||||
x.Actions().Add(ui.KeyActions{
|
||||
tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true),
|
||||
ui.KeySpace: ui.NewKeyAction("Expand/Collapse", x.noopCmd, true),
|
||||
ui.KeyX: ui.NewKeyAction("Expand/Collapse All", x.toggleCollapseCmd, true),
|
||||
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false),
|
||||
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", x.eraseCmd, false),
|
||||
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", x.eraseCmd, false),
|
||||
|
|
@ -127,25 +113,9 @@ func (x *Xray) bindKeys() {
|
|||
})
|
||||
}
|
||||
|
||||
func (x *Xray) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
||||
key := evt.Key()
|
||||
if key == tcell.KeyRune {
|
||||
if x.cmdBuff.IsActive() {
|
||||
x.cmdBuff.Add(evt.Rune())
|
||||
x.ClearSelection()
|
||||
x.update(x.filter(x.model.Peek()))
|
||||
x.UpdateTitle()
|
||||
return nil
|
||||
}
|
||||
|
||||
key = mapKey(evt)
|
||||
}
|
||||
|
||||
if a, ok := x.actions[key]; ok {
|
||||
return a.Action(evt)
|
||||
}
|
||||
|
||||
return evt
|
||||
func (x *Xray) keyEntered() {
|
||||
x.ClearSelection()
|
||||
x.update(x.filter(x.model.Peek()))
|
||||
}
|
||||
|
||||
func (x *Xray) refreshActions() {
|
||||
|
|
@ -155,11 +125,11 @@ func (x *Xray) refreshActions() {
|
|||
pluginActions(x, aa)
|
||||
hotKeyActions(x, aa)
|
||||
|
||||
x.actions.Add(aa)
|
||||
x.Actions().Add(aa)
|
||||
x.app.Menu().HydrateMenu(x.Hints())
|
||||
}()
|
||||
|
||||
x.actions.Clear()
|
||||
x.Actions().Clear()
|
||||
x.bindKeys()
|
||||
|
||||
ref := x.selectedSpec()
|
||||
|
|
@ -191,11 +161,11 @@ func (x *Xray) refreshActions() {
|
|||
aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
|
||||
}
|
||||
|
||||
x.actions.Add(aa)
|
||||
x.Actions().Add(aa)
|
||||
}
|
||||
|
||||
// GetSelectedItem returns the current selection as string.
|
||||
func (x *Xray) GetSelectedItem() string {
|
||||
// GetSelectedPath returns the current selection as string.
|
||||
func (x *Xray) GetSelectedPath() string {
|
||||
ref := x.selectedSpec()
|
||||
if ref == nil {
|
||||
return ""
|
||||
|
|
@ -203,16 +173,6 @@ func (x *Xray) GetSelectedItem() string {
|
|||
return ref.Path
|
||||
}
|
||||
|
||||
// EnvFn returns an plugin env function if available.
|
||||
func (x *Xray) EnvFn() EnvFunc {
|
||||
return x.envFn
|
||||
}
|
||||
|
||||
// Aliases returns all available aliases.
|
||||
func (x *Xray) Aliases() []string {
|
||||
return append(x.meta.ShortNames, x.meta.SingularName, x.meta.Name)
|
||||
}
|
||||
|
||||
func (x *Xray) selectedSpec() *xray.NodeSpec {
|
||||
node := x.GetCurrentNode()
|
||||
if node == nil {
|
||||
|
|
@ -228,6 +188,16 @@ func (x *Xray) selectedSpec() *xray.NodeSpec {
|
|||
return &ref
|
||||
}
|
||||
|
||||
// EnvFn returns an plugin env function if available.
|
||||
func (x *Xray) EnvFn() EnvFunc {
|
||||
return x.envFn
|
||||
}
|
||||
|
||||
// Aliases returns all available aliases.
|
||||
func (x *Xray) Aliases() []string {
|
||||
return append(x.meta.ShortNames, x.meta.SingularName, x.meta.Name)
|
||||
}
|
||||
|
||||
func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||
ref := x.selectedSpec()
|
||||
|
|
@ -246,7 +216,6 @@ func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (x *Xray) showLogs(pod, co *xray.NodeSpec, prev bool) {
|
||||
log.Debug().Msgf("SHOWING LOGS path %q", co.Path)
|
||||
// Need to load and wait for pods
|
||||
ns, _ := client.Namespaced(pod.Path)
|
||||
_, err := x.app.factory.CanForResource(ns, "v1/pods", client.MonitorAccess)
|
||||
|
|
@ -384,25 +353,21 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
func (x *Xray) noopCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
return evt
|
||||
}
|
||||
|
||||
func (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if x.app.InCmdMode() {
|
||||
return evt
|
||||
}
|
||||
x.app.Flash().Info("Filter mode activated.")
|
||||
x.cmdBuff.SetActive(true)
|
||||
x.CmdBuff().SetActive(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Xray) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !x.cmdBuff.IsActive() {
|
||||
if !x.CmdBuff().IsActive() {
|
||||
return evt
|
||||
}
|
||||
x.cmdBuff.Clear()
|
||||
x.CmdBuff().Clear()
|
||||
x.model.ClearFilter()
|
||||
x.Start()
|
||||
|
||||
|
|
@ -410,8 +375,8 @@ func (x *Xray) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (x *Xray) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if x.cmdBuff.IsActive() {
|
||||
x.cmdBuff.Delete()
|
||||
if x.CmdBuff().IsActive() {
|
||||
x.CmdBuff().Delete()
|
||||
}
|
||||
x.UpdateTitle()
|
||||
|
||||
|
|
@ -419,13 +384,13 @@ func (x *Xray) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !x.cmdBuff.InCmdMode() {
|
||||
x.cmdBuff.Reset()
|
||||
if !x.CmdBuff().InCmdMode() {
|
||||
x.CmdBuff().Reset()
|
||||
return x.app.PrevCmd(evt)
|
||||
}
|
||||
|
||||
x.app.Flash().Info("Clearing filter...")
|
||||
x.cmdBuff.Reset()
|
||||
x.CmdBuff().Reset()
|
||||
x.model.ClearFilter()
|
||||
x.Start()
|
||||
|
||||
|
|
@ -433,11 +398,11 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
|
||||
func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if x.cmdBuff.IsActive() {
|
||||
if ui.IsLabelSelector(x.cmdBuff.String()) {
|
||||
if x.CmdBuff().IsActive() {
|
||||
if ui.IsLabelSelector(x.CmdBuff().String()) {
|
||||
x.Start()
|
||||
}
|
||||
x.cmdBuff.SetActive(false)
|
||||
x.CmdBuff().SetActive(false)
|
||||
x.GetRoot().ExpandAll()
|
||||
|
||||
return nil
|
||||
|
|
@ -457,26 +422,9 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *Xray) toggleCollapseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
x.expandNodes = !x.expandNodes
|
||||
x.GetRoot().Walk(func(node, parent *tview.TreeNode) bool {
|
||||
if parent != nil {
|
||||
node.SetExpanded(x.expandNodes)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearSelection clears the currently selected node.
|
||||
func (x *Xray) ClearSelection() {
|
||||
x.selectedNode = ""
|
||||
x.SetCurrentNode(nil)
|
||||
}
|
||||
|
||||
func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode {
|
||||
q := x.cmdBuff.String()
|
||||
if x.cmdBuff.Empty() || ui.IsLabelSelector(q) {
|
||||
q := x.CmdBuff().String()
|
||||
if x.CmdBuff().Empty() || ui.IsLabelSelector(q) {
|
||||
return root
|
||||
}
|
||||
|
||||
|
|
@ -493,7 +441,7 @@ func (x *Xray) TreeNodeSelected() {
|
|||
x.app.QueueUpdateDraw(func() {
|
||||
n := x.GetCurrentNode()
|
||||
if n != nil {
|
||||
n.SetColor(config.AsColor(x.app.Styles.GetTable().CursorColor))
|
||||
n.SetColor(config.AsColor(x.app.Styles.Xray().CursorColor))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -504,7 +452,7 @@ func (x *Xray) TreeLoadFailed(err error) {
|
|||
}
|
||||
|
||||
func (x *Xray) update(node *xray.TreeNode) {
|
||||
root := makeTreeNode(node, x.expandNodes, x.app.Styles)
|
||||
root := makeTreeNode(node, x.ExpandNodes(), x.app.Styles)
|
||||
if node == nil {
|
||||
x.app.QueueUpdateDraw(func() {
|
||||
x.SetRoot(root)
|
||||
|
|
@ -515,8 +463,8 @@ func (x *Xray) update(node *xray.TreeNode) {
|
|||
for _, c := range node.Children {
|
||||
x.hydrate(root, c)
|
||||
}
|
||||
if x.selectedNode == "" {
|
||||
x.selectedNode = node.ID
|
||||
if x.GetSelectedItem() == "" {
|
||||
x.SetSelectedItem(node.ID)
|
||||
}
|
||||
|
||||
x.app.QueueUpdateDraw(func() {
|
||||
|
|
@ -529,12 +477,12 @@ func (x *Xray) update(node *xray.TreeNode) {
|
|||
}
|
||||
// BOZO!! Figure this out expand/collapse but the root
|
||||
if parent != nil {
|
||||
node.SetExpanded(x.expandNodes)
|
||||
node.SetExpanded(x.ExpandNodes())
|
||||
} else {
|
||||
node.SetExpanded(true)
|
||||
}
|
||||
|
||||
if ref.Path == x.selectedNode {
|
||||
if ref.Path == x.GetSelectedItem() {
|
||||
node.SetExpanded(true).SetSelectable(true)
|
||||
x.SetCurrentNode(node)
|
||||
}
|
||||
|
|
@ -545,13 +493,13 @@ func (x *Xray) update(node *xray.TreeNode) {
|
|||
|
||||
// TreeChanged notifies the model data changed.
|
||||
func (x *Xray) TreeChanged(node *xray.TreeNode) {
|
||||
x.count = node.Count(x.gvr.String())
|
||||
x.Count = node.Count(x.gvr.String())
|
||||
x.update(x.filter(node))
|
||||
x.UpdateTitle()
|
||||
}
|
||||
|
||||
func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
|
||||
node := makeTreeNode(n, x.expandNodes, x.app.Styles)
|
||||
node := makeTreeNode(n, x.ExpandNodes(), x.app.Styles)
|
||||
for _, c := range n.Children {
|
||||
x.hydrate(node, c)
|
||||
}
|
||||
|
|
@ -576,10 +524,10 @@ func (x *Xray) BufferActive(state bool, k ui.BufferKind) {
|
|||
func (x *Xray) defaultContext() context.Context {
|
||||
ctx := context.WithValue(context.Background(), internal.KeyFactory, x.app.factory)
|
||||
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
||||
if x.cmdBuff.Empty() {
|
||||
if x.CmdBuff().Empty() {
|
||||
ctx = context.WithValue(ctx, internal.KeyLabels, "")
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(x.cmdBuff.String()))
|
||||
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(x.CmdBuff().String()))
|
||||
}
|
||||
|
||||
return ctx
|
||||
|
|
@ -589,8 +537,8 @@ func (x *Xray) defaultContext() context.Context {
|
|||
func (x *Xray) Start() {
|
||||
x.Stop()
|
||||
|
||||
x.cmdBuff.AddListener(x.app.Cmd())
|
||||
x.cmdBuff.AddListener(x)
|
||||
x.CmdBuff().AddListener(x.app.Cmd())
|
||||
x.CmdBuff().AddListener(x)
|
||||
|
||||
ctx := x.defaultContext()
|
||||
ctx, x.cancelFn = context.WithCancel(ctx)
|
||||
|
|
@ -606,8 +554,8 @@ func (x *Xray) Stop() {
|
|||
x.cancelFn()
|
||||
x.cancelFn = nil
|
||||
|
||||
x.cmdBuff.RemoveListener(x.app.Cmd())
|
||||
x.cmdBuff.RemoveListener(x)
|
||||
x.CmdBuff().RemoveListener(x.app.Cmd())
|
||||
x.CmdBuff().RemoveListener(x)
|
||||
}
|
||||
|
||||
// SetBindKeysFn sets up extra key bindings.
|
||||
|
|
@ -645,12 +593,12 @@ func (x *Xray) styleTitle() string {
|
|||
ns = client.NamespaceAll
|
||||
}
|
||||
|
||||
buff := x.cmdBuff.String()
|
||||
buff := x.CmdBuff().String()
|
||||
var title string
|
||||
if ns == client.ClusterScope {
|
||||
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, x.count), x.app.Styles.Frame())
|
||||
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, x.Count), x.app.Styles.Frame())
|
||||
} else {
|
||||
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, x.count), x.app.Styles.Frame())
|
||||
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, x.Count), x.app.Styles.Frame())
|
||||
}
|
||||
if buff == "" {
|
||||
return title
|
||||
|
|
@ -690,14 +638,6 @@ func (x *Xray) resourceDelete(gvr client.GVR, ref *xray.NodeSpec, msg string) {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func mapKey(evt *tcell.EventKey) tcell.Key {
|
||||
key := tcell.Key(evt.Rune())
|
||||
if evt.Modifiers() == tcell.ModAlt {
|
||||
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func fuzzyFilter(q, path string) bool {
|
||||
q = strings.TrimSpace(q[2:])
|
||||
mm := fuzzy.Find(q, []string{path})
|
||||
|
|
@ -720,7 +660,7 @@ func rxFilter(q, path string) bool {
|
|||
func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tview.TreeNode {
|
||||
n := tview.NewTreeNode("No data...")
|
||||
if node != nil {
|
||||
n.SetText(node.Title())
|
||||
n.SetText(node.Title(styles.Xray()))
|
||||
spec := xray.NodeSpec{}
|
||||
if p := node.Parent; p != nil {
|
||||
spec.GVR, spec.Path = p.GVR, p.ID
|
||||
|
|
@ -733,7 +673,7 @@ func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tv
|
|||
}
|
||||
n.SetSelectable(true)
|
||||
n.SetExpanded(expanded)
|
||||
n.SetColor(config.AsColor(styles.GetTable().CursorColor))
|
||||
n.SetColor(config.AsColor(styles.Xray().CursorColor))
|
||||
n.SetSelectedFunc(func() {
|
||||
n.SetExpanded(!n.IsExpanded())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/util/node"
|
||||
)
|
||||
|
||||
// Pod represents an xray renderer.
|
||||
|
|
@ -55,9 +54,11 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
|
|||
}
|
||||
|
||||
func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
|
||||
phase := p.phase(&po)
|
||||
var re render.Pod
|
||||
|
||||
phase := re.Phase(&po)
|
||||
ss := po.Status.ContainerStatuses
|
||||
cr, _, _ := p.statuses(ss)
|
||||
cr, _, _ := re.Statuses(ss)
|
||||
status := OkStatus
|
||||
if cr != len(ss) {
|
||||
status = ToastStatus
|
||||
|
|
@ -130,98 +131,3 @@ func (*Pod) podVolumeRefs(f dao.Factory, parent *TreeNode, ns string, vv []v1.Vo
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BOZO!! Dedup...
|
||||
func (*Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
|
||||
for _, c := range ss {
|
||||
if c.State.Terminated != nil {
|
||||
ct++
|
||||
}
|
||||
if c.Ready {
|
||||
cr = cr + 1
|
||||
}
|
||||
rc += int(c.RestartCount)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Pod) phase(po *v1.Pod) string {
|
||||
status := string(po.Status.Phase)
|
||||
if po.Status.Reason != "" {
|
||||
if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason {
|
||||
return "Unknown"
|
||||
}
|
||||
status = po.Status.Reason
|
||||
}
|
||||
|
||||
status, ok := p.initContainerPhase(po.Status, len(po.Spec.InitContainers), status)
|
||||
if ok {
|
||||
return status
|
||||
}
|
||||
|
||||
status, ok = p.containerPhase(po.Status, status)
|
||||
if ok && status == "Completed" {
|
||||
status = "Running"
|
||||
}
|
||||
if po.DeletionTimestamp == nil {
|
||||
return status
|
||||
}
|
||||
|
||||
return "Terminated"
|
||||
}
|
||||
|
||||
func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) {
|
||||
var running bool
|
||||
for i := len(st.ContainerStatuses) - 1; i >= 0; i-- {
|
||||
cs := st.ContainerStatuses[i]
|
||||
switch {
|
||||
case cs.State.Waiting != nil && cs.State.Waiting.Reason != "":
|
||||
status = cs.State.Waiting.Reason
|
||||
case cs.State.Terminated != nil && cs.State.Terminated.Reason != "":
|
||||
status = cs.State.Terminated.Reason
|
||||
case cs.State.Terminated != nil:
|
||||
if cs.State.Terminated.Signal != 0 {
|
||||
status = "Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal))
|
||||
} else {
|
||||
status = "ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode))
|
||||
}
|
||||
case cs.Ready && cs.State.Running != nil:
|
||||
running = true
|
||||
}
|
||||
}
|
||||
|
||||
return status, running
|
||||
}
|
||||
|
||||
func (p *Pod) initContainerPhase(st v1.PodStatus, initCount int, status string) (string, bool) {
|
||||
for i, cs := range st.InitContainerStatuses {
|
||||
s := checkContainerStatus(cs, i, initCount)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
return status, false
|
||||
}
|
||||
|
||||
func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string {
|
||||
switch {
|
||||
case cs.State.Terminated != nil:
|
||||
if cs.State.Terminated.ExitCode == 0 {
|
||||
return ""
|
||||
}
|
||||
if cs.State.Terminated.Reason != "" {
|
||||
return "Init:" + cs.State.Terminated.Reason
|
||||
}
|
||||
if cs.State.Terminated.Signal != 0 {
|
||||
return "Init:Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal))
|
||||
}
|
||||
return "Init:ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode))
|
||||
case cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing":
|
||||
return "Init:" + cs.State.Waiting.Reason
|
||||
default:
|
||||
return "Init:" + strconv.Itoa(i) + "/" + strconv.Itoa(initCount)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog/log"
|
||||
"vbom.ml/util/sortorder"
|
||||
|
|
@ -295,8 +296,8 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode {
|
|||
}
|
||||
|
||||
// Title computes the node title.
|
||||
func (t *TreeNode) Title() string {
|
||||
return t.toTitle()
|
||||
func (t *TreeNode) Title(styles config.Xray) string {
|
||||
return t.computeTitle(styles)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
@ -343,6 +344,14 @@ func category(gvr string) string {
|
|||
return meta.SingularName
|
||||
}
|
||||
|
||||
func (t TreeNode) computeTitle(styles config.Xray) string {
|
||||
if styles.ShowIcons {
|
||||
return t.toEmojiTitle()
|
||||
}
|
||||
|
||||
return t.toTitle()
|
||||
}
|
||||
|
||||
const (
|
||||
titleFmt = " [gray::-]%s/[white::b][%s::b]%s[::]"
|
||||
topTitleFmt = " [white::b][%s::b]%s[::]"
|
||||
|
|
@ -360,10 +369,9 @@ func (t TreeNode) toTitle() (title string) {
|
|||
color, status = "orange", toast+"_REF"
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if status != "OK" {
|
||||
title += fmt.Sprintf(" [gray::-][[%s::b]%s[gray::-]]", color, status)
|
||||
title += fmt.Sprintf(" [gray::-][yellow:%s:b]%s[gray::-]", color, status)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -382,7 +390,96 @@ func (t TreeNode) toTitle() (title string) {
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
title += fmt.Sprintf(" [antiquewhite::][%s][::]", info)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const colorFmt = "%s [%s::b]%s[::]"
|
||||
|
||||
func (t TreeNode) toEmojiTitle() (title string) {
|
||||
_, n := client.Namespaced(t.ID)
|
||||
color, status := "white", "OK"
|
||||
if v, ok := t.Extras[StatusKey]; ok {
|
||||
switch v {
|
||||
case ToastStatus:
|
||||
color, status = "orangered", toast
|
||||
case MissingRefStatus:
|
||||
color, status = "orange", toast+"_REF"
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if status != "OK" {
|
||||
title += fmt.Sprintf(" [gray::-][yellow:%s:b]%s[gray::-]", color, status)
|
||||
}
|
||||
}()
|
||||
|
||||
title = fmt.Sprintf(colorFmt, toEmoji(t.GVR), color, n)
|
||||
if !t.IsLeaf() {
|
||||
title += fmt.Sprintf("[white::d](%d[-::d])[-::-]", t.CountChildren())
|
||||
}
|
||||
|
||||
info, ok := t.Extras[InfoKey]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
title += fmt.Sprintf(" [antiquewhite::][%s][::]", info)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func toEmoji(gvr string) string {
|
||||
switch gvr {
|
||||
case "containers":
|
||||
return "🐳"
|
||||
case "v1/namespaces", "namespaces":
|
||||
return "🗂"
|
||||
case "v1/pods", "pods":
|
||||
return "🚛"
|
||||
case "v1/services", "services":
|
||||
return "💁♀️"
|
||||
case "v1/serviceaccounts", "serviceaccounts":
|
||||
return "💳"
|
||||
case "v1/persistentvolumes", "persistentvolumes":
|
||||
return "📚"
|
||||
case "v1/persistentvolumeclaims", "persistentvolumeclaims":
|
||||
return "🎟"
|
||||
case "v1/secrets", "secrets":
|
||||
return "🔒"
|
||||
case "v1/configmaps", "configmaps":
|
||||
return "🗺"
|
||||
case "apps/v1/deployments", "deployments":
|
||||
return "🪂"
|
||||
case "apps/v1/statefulsets", "statefulsets":
|
||||
return "🎎"
|
||||
case "apps/v1/daemonsets", "daemonsets":
|
||||
return "😈"
|
||||
default:
|
||||
return "📎"
|
||||
}
|
||||
}
|
||||
|
||||
// EmojiInfo returns emoji help.
|
||||
func EmojiInfo() map[string]string {
|
||||
gvrs := []string{
|
||||
"containers",
|
||||
"v1/namespaces",
|
||||
"v1/pods",
|
||||
"v1/services",
|
||||
"v1/serviceaccounts",
|
||||
"v1/persistentvolumes",
|
||||
"v1/persistentvolumeclaims",
|
||||
"v1/secrets",
|
||||
"v1/configmaps",
|
||||
"apps/v1/deployments",
|
||||
"apps/v1/statefulsets",
|
||||
"apps/v1/daemonsets",
|
||||
}
|
||||
|
||||
m := make(map[string]string, len(gvrs))
|
||||
for _, g := range gvrs {
|
||||
m[client.NewGVR(g).R()] = toEmoji(g)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ k9s:
|
|||
fgColor: darkgray
|
||||
bgColor: black
|
||||
sorterColor: white
|
||||
xray:
|
||||
fgColor: white
|
||||
bgColor: black
|
||||
cursorColor: whitesmoke
|
||||
graphicColor: gray
|
||||
emojiOn: true
|
||||
views:
|
||||
yaml:
|
||||
keyColor: ghostwhite
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ k9s:
|
|||
fgColor: *foreground
|
||||
bgColor: *background
|
||||
sorterColor: *cyan
|
||||
# Xray view attributes.
|
||||
xray:
|
||||
fgColor: *foreground
|
||||
bgColor: *background
|
||||
cursorColor: *current_line
|
||||
graphicColor: *purple
|
||||
emojiOn: false
|
||||
views:
|
||||
# YAML info styles.
|
||||
yaml:
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ k9s:
|
|||
fgColor: white
|
||||
bgColor: darkblue
|
||||
sorterColor: orange
|
||||
xray:
|
||||
fgColor: blue
|
||||
bgColor: darkblue
|
||||
cursorColor: aqua
|
||||
graphicColor: mediumslateblue
|
||||
emojiOn: false
|
||||
views:
|
||||
yaml:
|
||||
keyColor: steelblue
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ k9s:
|
|||
fgColor: white
|
||||
bgColor: "#282a36"
|
||||
sorterColor: orange
|
||||
xray:
|
||||
fgColor: "#57c7ff"
|
||||
bgColor: "#282a36"
|
||||
cursorColor: "#5af78e"
|
||||
graphicColor: darkgoldenrod
|
||||
emojiOn: false
|
||||
views:
|
||||
yaml:
|
||||
keyColor: "#ff5c57"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ k9s:
|
|||
fgColor: white
|
||||
bgColor: black
|
||||
sorterColor: orange
|
||||
xray:
|
||||
fgColor: blue
|
||||
bgColor: black
|
||||
cursorColor: aqua
|
||||
graphicColor: darkgoldenrod
|
||||
emojiOn: false
|
||||
views:
|
||||
yaml:
|
||||
keyColor: steelblue
|
||||
|
|
|
|||
Loading…
Reference in New Issue