diff --git a/change_logs/release_v0.13.0.md b/change_logs/release_v0.13.0.md index bb94762c..968b9dc0 100644 --- a/change_logs/release_v0.13.0.md +++ b/change_logs/release_v0.13.0.md @@ -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. diff --git a/cmd/root.go b/cmd/root.go index f3152a0e..6a273530 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 { diff --git a/internal/client/client.go b/internal/client/client.go index 706ff94e..0dc37b78 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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 } diff --git a/internal/client/config.go b/internal/client/config.go index 30c0ae8d..84af8ffd 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -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 } diff --git a/internal/client/metrics.go b/internal/client/metrics.go index a41e40cd..887d5b84 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -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 diff --git a/internal/client/metrics_test.go b/internal/client/metrics_test.go index 17331135..4244f7e8 100644 --- a/internal/client/metrics_test.go +++ b/internal/client/metrics_test.go @@ -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++ { diff --git a/internal/client/types.go b/internal/client/types.go index a23348ec..79ad0be3 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -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 diff --git a/internal/config/styles.go b/internal/config/styles.go index 8f3f5da0..b7cde097 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -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 diff --git a/internal/config/styles_test.go b/internal/config/styles_test.go index 56c2d78d..ee9fe85a 100644 --- a/internal/config/styles_test.go +++ b/internal/config/styles_test.go @@ -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) diff --git a/internal/dao/container.go b/internal/dao/container.go index 12a6cec2..c36c4c26 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -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) diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 3881906a..7e7efd0b 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -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{}) diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 92669433..43ab784a 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -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 diff --git a/internal/dao/ds.go b/internal/dao/ds.go index d073f976..c957766b 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -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 diff --git a/internal/dao/generic.go b/internal/dao/generic.go index c3fa704c..4d60762d 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -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 { diff --git a/internal/dao/node.go b/internal/dao/node.go index 338a2f77..17c33f25 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -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{ diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 83e7ad71..8a382e3f 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -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 diff --git a/internal/dao/port_forward.go b/internal/dao/port_forward.go index befdb3e8..02852886 100644 --- a/internal/dao/port_forward.go +++ b/internal/dao/port_forward.go @@ -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 diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index a2f77cc8..0b31cf19 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -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"} diff --git a/internal/dao/sts.go b/internal/dao/sts.go index b3870d0f..3fe02c52 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -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 diff --git a/internal/model/log.go b/internal/model/log.go index f7354aaf..7930e083 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -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) } diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go index 9a077ef5..37406fa9 100644 --- a/internal/model/stack_test.go +++ b/internal/model/stack_test.go @@ -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) {} diff --git a/internal/model/table.go b/internal/model/table.go index e8526670..8d320c26 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -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 } } diff --git a/internal/model/types.go b/internal/model/types.go index 5dd6c546..b8524dca 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -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. diff --git a/internal/render/generic.go b/internal/render/generic.go index 7e3f8119..d1e70bd9 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -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 } diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index e5f1187e..cb83209e 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -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"}, diff --git a/internal/render/pod.go b/internal/render/pod.go index dbd299a4..6a36e764 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -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 { diff --git a/internal/ui/app.go b/internal/ui/app.go index 1ae8cd7d..e200f282 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -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. diff --git a/internal/ui/config.go b/internal/ui/config.go index 8c04f158..53115f06 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -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 } diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index 3d3c807f..33149968 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -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 } diff --git a/internal/ui/table.go b/internal/ui/table.go index b425ef75..cad520b8 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -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) diff --git a/internal/ui/tree.go b/internal/ui/tree.go new file mode 100644 index 00000000..b4c69437 --- /dev/null +++ b/internal/ui/tree.go @@ -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 +} diff --git a/internal/view/app.go b/internal/view/app.go index 63c6d8e1..1d48a7fb 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -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() diff --git a/internal/view/browser.go b/internal/view/browser.go index 1ea8611f..ca1e0d94 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -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)) } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index dfa0f968..06f9cba8 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -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 } diff --git a/internal/view/details.go b/internal/view/details.go index 7f846eea..c1e52f67 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -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), diff --git a/internal/view/help.go b/internal/view/help.go index 90ead948..ca257204 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -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)) +} diff --git a/internal/view/log.go b/internal/view/log.go index ba90418d..db1c7c60 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -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. diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index 2e4ec250..80d274ec 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -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) diff --git a/internal/view/picker.go b/internal/view/picker.go index b0dacb25..8f37295f 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -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) } } diff --git a/internal/view/pod.go b/internal/view/pod.go index 395fbb00..14e3e0a9 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -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) diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index 07fdf4bf..375c2fe3 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -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 } diff --git a/internal/view/port_forward_test.go b/internal/view/port_forward_test.go index ad787bc5..fabe07b8 100644 --- a/internal/view/port_forward_test.go +++ b/internal/view/port_forward_test.go @@ -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())) } diff --git a/internal/view/svc.go b/internal/view/svc.go index f15a5d79..5cd7257b 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -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 diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index a6400b54..b1b4d364 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -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())) } diff --git a/internal/view/xray.go b/internal/view/xray.go index 4045892f..acdfcc15 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -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()) }) diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 39d74025..9a9e0cf6 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -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) - } -} diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 88021b8d..7e0c93ac 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -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 +} diff --git a/skins/black_and_wtf.yml b/skins/black_and_wtf.yml index e8d91b1a..aff4c8f1 100644 --- a/skins/black_and_wtf.yml +++ b/skins/black_and_wtf.yml @@ -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 diff --git a/skins/dracula.yml b/skins/dracula.yml index efd13fd6..5fd6b35e 100644 --- a/skins/dracula.yml +++ b/skins/dracula.yml @@ -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: diff --git a/skins/in_the_navy.yml b/skins/in_the_navy.yml index 9dc0f062..3e8b9898 100644 --- a/skins/in_the_navy.yml +++ b/skins/in_the_navy.yml @@ -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 diff --git a/skins/snazzy.yml b/skins/snazzy.yml index 3e1e285e..d90885bf 100644 --- a/skins/snazzy.yml +++ b/skins/snazzy.yml @@ -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" diff --git a/skins/stock.yml b/skins/stock.yml index be02aa01..3d307466 100644 --- a/skins/stock.yml +++ b/skins/stock.yml @@ -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