checkpoint

mine
derailed 2020-01-22 10:23:59 -07:00
parent cf79622be0
commit a96fa843b5
52 changed files with 863 additions and 482 deletions

View File

@ -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"/>

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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++ {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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{})

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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{

View File

@ -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

View File

@ -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

View File

@ -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"}

View File

@ -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

View File

@ -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)
}

View File

@ -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) {}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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"},

View File

@ -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 {

View File

@ -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.

View File

@ -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
}

View File

@ -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 }

View File

@ -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)

146
internal/ui/tree.go Normal file
View File

@ -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
}

View File

@ -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()

View File

@ -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))
}

View File

@ -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
}

View File

@ -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),

View File

@ -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))
}

View File

@ -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.

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()))
}

View File

@ -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

View File

@ -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()))
}

View File

@ -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())
})

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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"

View File

@ -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