From a5245a98de66841c62d697f77554f00be751c9db Mon Sep 17 00:00:00 2001 From: derailed Date: Wed, 29 May 2019 17:32:18 -0600 Subject: [PATCH] some bugs and perf fixes --- cmd/root.go | 2 +- go.mod | 5 +- go.sum | 4 + internal/config/bench.go | 123 ++++++++++++++ internal/config/bench_test.go | 155 ++++++++++++++++++ internal/config/config.go | 2 +- internal/config/style.go | 4 +- internal/config/test_assets/b_containers.yml | 66 ++++++++ internal/config/test_assets/b_good.yml | 35 ++++ internal/config/test_assets/b_toast.yml | 16 ++ internal/k8s/api.go | 3 + internal/k8s/port_forward.go | 12 +- internal/resource/base.go | 5 + internal/resource/cm.go | 7 + internal/resource/container.go | 11 ++ internal/resource/dp.go | 21 ++- internal/resource/job.go | 22 ++- internal/resource/list.go | 19 ++- internal/resource/no.go | 12 ++ internal/resource/pod.go | 11 ++ internal/resource/secret.go | 7 + internal/resource/sts.go | 8 + internal/views/app.go | 47 ++++-- internal/views/bench.go | 65 ++++---- internal/views/bench_test.go | 8 +- internal/views/benchmark.go | 14 +- internal/views/cluster_info.go | 8 +- internal/views/cmd_stack.go | 1 - internal/views/command.go | 7 +- internal/views/container.go | 64 ++++---- internal/views/context.go | 10 +- internal/views/cronjob.go | 5 +- internal/views/dp.go | 7 +- internal/views/ds.go | 9 +- internal/views/forward.go | 154 ++++++++++++----- internal/views/helpers.go | 8 + internal/views/job.go | 18 +- internal/views/logo.go | 6 +- internal/views/no.go | 5 +- internal/views/ns.go | 11 +- internal/views/pod.go | 101 +++++++----- internal/views/rbac.go | 5 +- internal/views/resource.go | 128 +++++++-------- internal/views/rs.go | 5 +- internal/views/secret.go | 5 +- internal/views/sorter.go | 4 +- internal/views/sorter_test.go | 15 +- internal/views/sts.go | 5 +- internal/views/svc.go | 15 +- internal/views/table.go | 16 +- internal/views/{assets => test_assets}/b1.txt | 0 internal/views/{assets => test_assets}/b2.txt | 0 internal/views/{assets => test_assets}/b3.txt | 0 internal/views/{assets => test_assets}/b4.txt | 0 internal/watch/container_test.go | 51 +++++- internal/watch/helper_test.go | 93 +++++++++++ internal/watch/helpers.go | 42 +++++ internal/watch/{meta.go => informer.go} | 88 +++++----- internal/watch/informer_test.go | 91 ++++++++++ internal/watch/meta_test.go | 66 -------- internal/watch/no_mx.go | 47 +++--- internal/watch/no_mx_test.go | 85 +++++++++- internal/watch/no_test.go | 4 +- internal/watch/pod.go | 38 ----- internal/watch/pod_mx.go | 47 +++--- internal/watch/pod_mx_test.go | 87 +++++++++- internal/watch/pod_test.go | 2 +- main.go | 5 + 68 files changed, 1493 insertions(+), 549 deletions(-) create mode 100644 internal/config/bench.go create mode 100644 internal/config/bench_test.go create mode 100644 internal/config/test_assets/b_containers.yml create mode 100644 internal/config/test_assets/b_good.yml create mode 100644 internal/config/test_assets/b_toast.yml rename internal/views/{assets => test_assets}/b1.txt (100%) rename internal/views/{assets => test_assets}/b2.txt (100%) rename internal/views/{assets => test_assets}/b3.txt (100%) rename internal/views/{assets => test_assets}/b4.txt (100%) rename internal/watch/{meta.go => informer.go} (51%) create mode 100644 internal/watch/informer_test.go delete mode 100644 internal/watch/meta_test.go diff --git a/cmd/root.go b/cmd/root.go index d758b72b..dae66362 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -75,7 +75,7 @@ func run(cmd *cobra.Command, args []string) { cfg := loadConfiguration() app := views.NewApp(cfg) { - defer app.Stop() + defer app.BailOut() app.Init(version, refreshRate, k8sFlags) app.Run() } diff --git a/go.mod b/go.mod index 38b189f6..fa73c975 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ replace ( ) require ( + cloud.google.com/go v0.34.0 github.com/Azure/go-autorest/autorest v0.1.0 // indirect github.com/derailed/tview v0.1.6 github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect @@ -25,6 +26,7 @@ require ( github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect github.com/google/btree v1.0.0 // indirect github.com/google/gofuzz v1.0.0 // indirect + github.com/googleapis/gax-go v2.0.2+incompatible // indirect github.com/googleapis/gnostic v0.2.0 // indirect github.com/gophercloud/gophercloud v0.0.0-20190427020117-60507118a582 // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect @@ -37,6 +39,7 @@ require ( github.com/onsi/gomega v1.5.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petergtz/pegomock v0.0.0-20181206220228-b113d17a7e81 + github.com/pkg/profile v1.3.0 github.com/rakyll/hey v0.1.2 github.com/rs/zerolog v1.14.3 github.com/spf13/cobra v0.0.3 @@ -47,7 +50,7 @@ require ( golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a // indirect golang.org/x/sys v0.0.0-20190426135247-a129542de9ae // indirect - golang.org/x/text v0.3.2 // indirect + golang.org/x/text v0.3.2 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect google.golang.org/appengine v1.5.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 2bea4440..74e133b1 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSN github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= +github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= @@ -198,6 +200,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.3.0 h1:OQIvuDgm00gWVWGTf4m4mCt6W1/0YqU7Ntg0mySWgaI= +github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/config/bench.go b/internal/config/bench.go new file mode 100644 index 00000000..3d79410d --- /dev/null +++ b/internal/config/bench.go @@ -0,0 +1,123 @@ +package config + +import ( + "io/ioutil" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +var ( + // K9sBenchmarks the name of the benchmarks config file. + K9sBenchmarks = "benchmarks.yml" + // K9sBenchmarkFile represents K9s config file location. + K9sBenchmarkFile = filepath.Join(K9sHome, K9sBenchmarks) +) + +type ( + // Bench tracks K9s styling options. + Bench struct { + Benchmarks *Benchmarks `yaml:"benchmarks"` + } + + // Benchmarks tracks K9s benchmarks configuration. + Benchmarks struct { + Defaults Benchmark `yaml:"defaults"` + Services map[string]BenchConfig `yam':"services"` + Containers map[string]BenchConfig `yam':"containers"` + } + + // Auth basic auth creds + Auth struct { + User string `yaml:"user"` + Password string `yaml:"password"` + } + + // Benchmark represents a generic benchmark. + Benchmark struct { + C int `yaml:"concurrency"` + N int `yaml:"requests"` + } + + // BenchConfig represents a service benchmark. + BenchConfig struct { + C int `yaml:"concurrency"` + N int `yaml:"requests"` + Method string `yaml:"method"` + Name string `yaml:"name"` + Address string `yaml:"address"` + Path string `yaml:"path"` + HTTP2 bool `yaml:"http2"` + Body string `yaml:"body"` + Auth Auth `yaml:"auth"` + Headers []string `yaml:"headers"` + } +) + +const ( + // DefaultC default concurrency. + DefaultC = 1 + // DefaultN default number of requests. + DefaultN = 200 + // DefaultMethod default http verb. + DefaultMethod = "GET" +) + +func newBenchmark() Benchmark { + return Benchmark{ + C: DefaultC, + N: DefaultN, + } +} + +func (b Benchmark) empty() bool { + return b.C == 0 && b.N == 0 +} + +func newBenchmarks() *Benchmarks { + return &Benchmarks{ + Defaults: newBenchmark(), + } +} + +func newBench() *Bench { + return &Bench{ + Benchmarks: newBenchmarks(), + } +} + +// NewBench creates a new default config. +func NewBench(file string) (*Bench, error) { + s := &Bench{Benchmarks: newBenchmarks()} + err := s.load(file) + return s, err +} + +// Reload update the configuration from disk. +func (s *Bench) Reload() error { + return s.load(K9sBenchmarkFile) +} + +// Load K9s benchmark configs from file +func (s *Bench) load(path string) error { + f, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + if err := yaml.Unmarshal(f, &s); err != nil { + return err + } + // s.fill() + + return nil +} + +// func (s *Bench) fill() { +// for k, svc := range s.Benchmarks.Services { +// if svc.Benchmark.empty() { +// svc.Benchmark = +// s.Benchmarks.Services[k] = svc +// } +// } +// } diff --git a/internal/config/bench_test.go b/internal/config/bench_test.go new file mode 100644 index 00000000..b2e515c0 --- /dev/null +++ b/internal/config/bench_test.go @@ -0,0 +1,155 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBenchLoad(t *testing.T) { + uu := map[string]struct { + file string + c, n int + svcCount int + coCount int + }{ + "goodConfig": { + "test_assets/b_good.yml", + 2, + 1000, + 2, + 0, + }, + "malformed": { + "test_assets/b_toast.yml", + 1, + 200, + 0, + 0, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + b, err := NewBench(u.file) + + assert.Nil(t, err) + assert.Equal(t, u.c, b.Benchmarks.Defaults.C) + assert.Equal(t, u.n, b.Benchmarks.Defaults.N) + assert.Equal(t, u.svcCount, len(b.Benchmarks.Services)) + assert.Equal(t, u.coCount, len(b.Benchmarks.Containers)) + }) + } +} + +func TestBenchServiceLoad(t *testing.T) { + uu := map[string]struct { + key string + c, n int + method, address, path string + http2 bool + body string + auth Auth + headers []string + }{ + "s1": { + "default/nginx", + 2, + 1000, + "GET", + "10.10.10.10", + "/", + true, + `{"fred": "blee"}`, + Auth{"fred", "blee"}, + []string{"Accept: text/html", "Content-Type: application/json"}, + }, + "s2": { + "blee/fred", + 10, + 1500, + "POST", + "20.20.20.20", + "/zorg", + false, + `{"fred": "blee"}`, + Auth{"fred", "blee"}, + []string{"Accept: text/html", "Content-Type: application/json"}, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + b, err := NewBench("test_assets/b_good.yml") + + assert.Nil(t, err) + assert.Equal(t, 2, len(b.Benchmarks.Services)) + svc := b.Benchmarks.Services[u.key] + assert.Equal(t, u.c, svc.C) + assert.Equal(t, u.n, svc.N) + assert.Equal(t, u.method, svc.Method) + assert.Equal(t, u.address, svc.Address) + assert.Equal(t, u.path, svc.Path) + assert.Equal(t, u.http2, svc.HTTP2) + assert.Equal(t, u.body, svc.Body) + assert.Equal(t, u.auth, svc.Auth) + assert.Equal(t, u.headers, svc.Headers) + }) + } +} + +func TestBenchContainerLoad(t *testing.T) { + uu := map[string]struct { + key string + c, n int + method, address, path string + http2 bool + body string + auth Auth + headers []string + }{ + "c1": { + "c1", + 2, + 1000, + "GET", + "10.10.10.10", + "/duh", + true, + `{"fred": "blee"}`, + Auth{"fred", "blee"}, + []string{"Accept: text/html", "Content-Type: application/json"}, + }, + "c2": { + "c2", + 10, + 1500, + "POST", + "20.20.20.20", + "/fred", + false, + `{"fred": "blee"}`, + Auth{"fred", "blee"}, + []string{"Accept: text/html", "Content-Type: application/json"}, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + b, err := NewBench("test_assets/b_containers.yml") + + assert.Nil(t, err) + assert.Equal(t, 2, len(b.Benchmarks.Services)) + co := b.Benchmarks.Containers[u.key] + assert.Equal(t, u.c, co.C) + assert.Equal(t, u.n, co.N) + assert.Equal(t, u.method, co.Method) + assert.Equal(t, u.address, co.Address) + assert.Equal(t, u.path, co.Path) + assert.Equal(t, u.http2, co.HTTP2) + assert.Equal(t, u.body, co.Body) + assert.Equal(t, u.auth, co.Auth) + assert.Equal(t, u.headers, co.Headers) + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index a5d2b6c8..f3967aaf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -196,7 +196,7 @@ func (c *Config) Validate() { // Dump debug... func (c *Config) Dump(msg string) { log.Debug().Msg(msg) - log.Debug().Msgf("Current Context: %s\n", c.K9s.CurrentCluster) + log.Debug().Msgf("Current Cluster: %s\n", c.K9s.CurrentCluster) for k, cl := range c.K9s.Clusters { log.Debug().Msgf("K9s cluster: %s -- %s\n", k, cl.Namespace) } diff --git a/internal/config/style.go b/internal/config/style.go index f73a2ced..991ace97 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -10,7 +10,7 @@ import ( ) var ( - // K9sStylesFile represents K9s config file location. + // K9sStylesFile represents K9s skins file location. K9sStylesFile = filepath.Join(K9sHome, "skin.yml") ) @@ -133,7 +133,7 @@ func newStatus() *Status { return &Status{ NewColor: "lightskyblue", ModifyColor: "greenyellow", - AddColor: "white", + AddColor: "dodgerblue", ErrorColor: "orangered", HighlightColor: "aqua", KillColor: "mediumpurple", diff --git a/internal/config/test_assets/b_containers.yml b/internal/config/test_assets/b_containers.yml new file mode 100644 index 00000000..d27dfa4b --- /dev/null +++ b/internal/config/test_assets/b_containers.yml @@ -0,0 +1,66 @@ +benchmarks: + defaults: + concurrency: 2 + requests: 1000 + containers: + c1: + concurrency: 2 + requests: 1000 + method: GET + http2: true + address: 10.10.10.10 + path: /duh + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" + c2: + concurrency: 10 + requests: 1500 + method: POST + http2: false + address: 20.20.20.20 + path: /fred + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" + services: + default/nginx: + concurrency: 2 + requests: 1000 + method: GET + http2: true + address: 10.10.10.10 + path: / + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" + blee/fred: + concurrency: 10 + requests: 1500 + method: POST + http2: false + address: 20.20.20.20 + path: /blee + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" \ No newline at end of file diff --git a/internal/config/test_assets/b_good.yml b/internal/config/test_assets/b_good.yml new file mode 100644 index 00000000..630aa285 --- /dev/null +++ b/internal/config/test_assets/b_good.yml @@ -0,0 +1,35 @@ +benchmarks: + defaults: + concurrency: 2 + requests: 1000 + services: + default/nginx: + concurrency: 2 + requests: 1000 + method: GET + http2: true + address: 10.10.10.10 + path: / + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" + blee/fred: + concurrency: 10 + requests: 1500 + method: POST + http2: false + address: 20.20.20.20 + path: /zorg + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" \ No newline at end of file diff --git a/internal/config/test_assets/b_toast.yml b/internal/config/test_assets/b_toast.yml new file mode 100644 index 00000000..1d0fa68d --- /dev/null +++ b/internal/config/test_assets/b_toast.yml @@ -0,0 +1,16 @@ +benchmarks: + service: + - default/nginx: + concurrency: 1 + requests: 100 + http2: true + method: GET + url: http://35.224.16.201/ + body: |- + {"fred": "blee"} + auth: + user: "fred" + password: "blee" + headers: + - "Accept: text/html" + - "Content-Type: application/json" \ No newline at end of file diff --git a/internal/k8s/api.go b/internal/k8s/api.go index 8f1eb65b..38c59e78 100644 --- a/internal/k8s/api.go +++ b/internal/k8s/api.go @@ -286,6 +286,9 @@ func (a *APIClient) SwitchContextOrDie(ctx string) { } func (a *APIClient) reset() { + a.mx.Lock() + defer a.mx.Unlock() + a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil } diff --git a/internal/k8s/port_forward.go b/internal/k8s/port_forward.go index d33de85c..65a94552 100644 --- a/internal/k8s/port_forward.go +++ b/internal/k8s/port_forward.go @@ -33,6 +33,7 @@ type PortForward struct { logger *zerolog.Logger active bool path string + container string ports []string age time.Time } @@ -57,6 +58,7 @@ func (p *PortForward) Active() bool { return p.active } +// SetActive mark a portforward as active. func (p *PortForward) SetActive(b bool) { p.active = b } @@ -71,6 +73,11 @@ func (p *PortForward) Path() string { return p.path } +// Container returns the targetes container. +func (p *PortForward) Container() string { + return p.container +} + // Stop terminates a port forard func (p *PortForward) Stop() { p.logger.Debug().Msgf("<<< Stopping port forward %q %v", p.path, p.ports) @@ -79,15 +86,14 @@ func (p *PortForward) Stop() { } // Start initiates a port foward session for a given pod and ports. -func (p *PortForward) Start(path string, ports []string) (*portforward.PortForwarder, error) { - p.path, p.ports, p.age = path, ports, time.Now() +func (p *PortForward) Start(path, co string, ports []string) (*portforward.PortForwarder, error) { + p.path, p.container, p.ports, p.age = path, co, ports, time.Now() ns, n := namespaced(path) pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{}) if err != nil { return nil, err } - if pod.Status.Phase != v1.PodRunning { return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) } diff --git a/internal/resource/base.go b/internal/resource/base.go index ffa8aba0..e77010d9 100644 --- a/internal/resource/base.go +++ b/internal/resource/base.go @@ -87,6 +87,11 @@ func (b *Base) Name() string { return b.path } +// NumCols designates if column is numerical. +func (*Base) NumCols(n string) map[string]bool { + return map[string]bool{} +} + // ExtFields returns extended fields in relation to headers. func (*Base) ExtFields() Properties { return Properties{} diff --git a/internal/resource/cm.go b/internal/resource/cm.go index 1d91fda8..3a79a815 100644 --- a/internal/resource/cm.go +++ b/internal/resource/cm.go @@ -73,6 +73,13 @@ func (*ConfigMap) Header(ns string) Row { return append(hh, "NAME", "DATA", "AGE") } +// NumCols designates if column is numerical. +func (*ConfigMap) NumCols(n string) map[string]bool { + return map[string]bool{ + "DATA": true, + } +} + // Fields retrieves displayable fields. func (r *ConfigMap) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) diff --git a/internal/resource/container.go b/internal/resource/container.go index 3ecee5bf..ae6eba95 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -164,6 +164,17 @@ func (*Container) Header(ns string) Row { ) } +// NumCols designates if column is numerical. +func (*Container) NumCols(n string) map[string]bool { + return map[string]bool{ + "CPU": true, + "MEM": true, + "%CPU": true, + "%MEM": true, + "RS": true, + } +} + // Fields retrieves displayable fields. func (r *Container) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) diff --git a/internal/resource/dp.go b/internal/resource/dp.go index 85629e4f..93594be2 100644 --- a/internal/resource/dp.go +++ b/internal/resource/dp.go @@ -65,12 +65,29 @@ func (r *Deployment) Marshal(path string) (string, error) { // Header return resource header. func (*Deployment) Header(ns string) Row { - hh := Row{} + var hh Row if ns == AllNamespaces { hh = append(hh, "NAMESPACE") } - return append(hh, "NAME", "DESIRED", "CURRENT", "UP-TO-DATE", "AVAILABLE", "AGE") + return append(hh, + "NAME", + "DESIRED", + "CURRENT", + "UP-TO-DATE", + "AVAILABLE", + "AGE", + ) +} + +// NumCols designates if column is numerical. +func (*Deployment) NumCols(n string) map[string]bool { + return map[string]bool{ + "DESIRED": true, + "CURRENT": true, + "UP-TO-DATE": true, + "AVAILABLE": true, + } } // Fields retrieves displayable fields. diff --git a/internal/resource/job.go b/internal/resource/job.go index 8300ae1f..4b59ea21 100644 --- a/internal/resource/job.go +++ b/internal/resource/job.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" "strings" + "sync" "time" "github.com/derailed/k9s/internal/k8s" @@ -18,7 +19,9 @@ import ( // Job tracks a kubernetes resource. type Job struct { *Base + instance *batchv1.Job + mx sync.RWMutex } // NewJobList returns a new resource list. @@ -33,7 +36,9 @@ func NewJobList(c Connection, ns string) List { // NewJob instantiates a new Job. func NewJob(c Connection) *Job { - j := &Job{&Base{Connection: c, Resource: k8s.NewJob(c)}, nil} + j := &Job{ + Base: &Base{Connection: c, Resource: k8s.NewJob(c)}, + } j.Factory = j return j @@ -87,7 +92,13 @@ func (r *Job) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (c go func() { select { case <-time.After(defaultTimeout): - if blocked { + var closes bool + r.mx.RLock() + { + closes = blocked + } + r.mx.RUnlock() + if closes { close(c) cancel() } @@ -96,11 +107,16 @@ func (r *Job) Logs(c chan<- string, ns, n, co string, lines int64, prev bool) (c // This call will block if nothing is in the stream!! stream, err := req.Stream() - blocked = false if err != nil { return cancel, fmt.Errorf("Log tail request failed for job `%s/%s:%s", ns, n, co) } + r.mx.Lock() + { + blocked = false + } + r.mx.Unlock() + go func() { defer func() { stream.Close() diff --git a/internal/resource/list.go b/internal/resource/list.go index 518ea431..babf3cff 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -57,6 +57,7 @@ type ( TableData struct { Header Row Rows RowEvents + NumCols map[string]bool Namespace string } @@ -68,7 +69,7 @@ type ( AllNamespaces() bool GetNamespace() string SetNamespace(string) - Reconcile(informer *wa.Meta, path *string) error + Reconcile(informer *wa.Informer, path *string) error GetName() string Access(flag int) bool GetAccess() int @@ -106,6 +107,7 @@ type ( Describe(kind, pa string, flags *genericclioptions.ConfigFlags) (string, error) Marshal(pa string) (string, error) Header(ns string) Row + NumCols(ns string) map[string]bool SetFieldSelector(string) SetLabelSelector(string) GetFieldSelector() string @@ -221,6 +223,7 @@ func (l *list) Data() TableData { return TableData{ Header: l.resource.Header(l.namespace), Rows: l.cache, + NumCols: l.resource.NumCols(l.namespace), Namespace: l.namespace, } } @@ -233,8 +236,8 @@ func metaFQN(m metav1.ObjectMeta) string { return fqn(m.Namespace, m.Name) } -func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) { - rr, err := m.List(l.name, ns, metav1.ListOptions{ +func (l *list) fetchFromStore(informer *wa.Informer, ns string) (Columnars, error) { + rr, err := informer.List(l.name, ns, metav1.ListOptions{ FieldSelector: l.resource.GetFieldSelector(), LabelSelector: l.resource.GetLabelSelector(), }) @@ -253,7 +256,7 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) { case *v1.Node: fqn = metaFQN(o.ObjectMeta) res = l.resource.New(r) - nmx, err := m.Get(wa.NodeMXIndex, fqn, opts) + nmx, err := informer.Get(wa.NodeMXIndex, fqn, opts) if err != nil { log.Warn().Err(err).Msg("NodeMetrics") } @@ -263,7 +266,7 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) { case *v1.Pod: fqn = metaFQN(o.ObjectMeta) res = l.resource.New(r) - pmx, err := m.Get(wa.PodMXIndex, fqn, opts) + pmx, err := informer.Get(wa.PodMXIndex, fqn, opts) if err != nil { log.Warn().Err(err).Msgf("PodMetrics %s", fqn) } @@ -273,7 +276,7 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) { case v1.Container: fqn = ns res = l.resource.New(r) - pmx, err := m.Get(wa.PodMXIndex, fqn, opts) + pmx, err := informer.Get(wa.PodMXIndex, fqn, opts) if err != nil { log.Warn().Err(err).Msgf("PodMetrics %s", fqn) } @@ -290,14 +293,14 @@ func (l *list) fetchFromStore(m *wa.Meta, ns string) (Columnars, error) { } // Reconcile previous vs current state and emits delta events. -func (l *list) Reconcile(m *wa.Meta, path *string) error { +func (l *list) Reconcile(informer *wa.Informer, path *string) error { ns := l.namespace if path != nil { ns = *path } var items Columnars - if rr, err := l.fetchFromStore(m, ns); err == nil { + if rr, err := l.fetchFromStore(informer, ns); err == nil { items = rr } else { items, err = l.resource.List(l.namespace) diff --git a/internal/resource/no.go b/internal/resource/no.go index 2c441170..c0e435a9 100644 --- a/internal/resource/no.go +++ b/internal/resource/no.go @@ -119,6 +119,18 @@ func (*Node) Header(ns string) Row { } } +// NumCols designates if column is numerical. +func (*Node) NumCols(n string) map[string]bool { + return map[string]bool{ + "CPU": true, + "MEM": true, + "%CPU": true, + "%MEM": true, + "ACPU": true, + "AMEM": true, + } +} + // Fields returns displayable fields. func (r *Node) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) diff --git a/internal/resource/pod.go b/internal/resource/pod.go index f57d656d..c22eb12f 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -208,6 +208,17 @@ func (*Pod) Header(ns string) Row { ) } +// NumCols designates if column is numerical. +func (*Pod) NumCols(n string) map[string]bool { + return map[string]bool{ + "CPU": true, + "MEM": true, + "%CPU": true, + "%MEM": true, + "RS": true, + } +} + // Fields retrieves displayable fields. func (r *Pod) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) diff --git a/internal/resource/secret.go b/internal/resource/secret.go index 9dc86df9..1e7115e5 100644 --- a/internal/resource/secret.go +++ b/internal/resource/secret.go @@ -73,6 +73,13 @@ func (*Secret) Header(ns string) Row { return append(hh, "NAME", "TYPE", "DATA", "AGE") } +// NumCols designates if column is numerical. +func (*Secret) NumCols(n string) map[string]bool { + return map[string]bool{ + "DATA": true, + } +} + // Fields retrieves displayable fields. func (r *Secret) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) diff --git a/internal/resource/sts.go b/internal/resource/sts.go index 312ab7f5..90b187f8 100644 --- a/internal/resource/sts.go +++ b/internal/resource/sts.go @@ -73,6 +73,14 @@ func (*StatefulSet) Header(ns string) Row { return append(hh, "NAME", "DESIRED", "CURRENT", "AGE") } +// NumCols designates if column is numerical. +func (*StatefulSet) NumCols(n string) map[string]bool { + return map[string]bool{ + "DESIRED": true, + "CURRENT": true, + } +} + // Fields retrieves displayable fields. func (r *StatefulSet) Fields(ns string) Row { ff := make(Row, 0, len(r.Header(ns))) diff --git a/internal/views/app.go b/internal/views/app.go index 80c8ae8c..779ee301 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -24,9 +24,10 @@ type ( focusHandler func(tview.Primitive) forwarder interface { - Start(path string, ports []string) (*portforward.PortForwarder, error) + Start(path, co string, ports []string) (*portforward.PortForwarder, error) Stop() Path() string + Container() string Ports() []string Active() bool Age() string @@ -59,6 +60,7 @@ type ( config *config.Config styles *config.Styles + bench *config.Bench version string flags *genericclioptions.ConfigFlags pages *tview.Pages @@ -78,8 +80,9 @@ type ( cmdBuff *cmdBuff actions keyActions stopCh chan struct{} - informer *watch.Meta + informer *watch.Informer forwarders []forwarder + hasSkins bool } ) @@ -94,6 +97,7 @@ func NewApp(cfg *config.Config) *appView { cmdBuff: newCmdBuff(':'), } { + v.initBench() v.refreshStyles() v.menuView = newMenuView(&v) v.logoView = newLogoView(&v) @@ -160,29 +164,36 @@ func (a *appView) startInformer() { if a.flags.Namespace != nil { ns = *a.flags.Namespace } - a.informer = watch.NewMeta(a.conn(), ns) - log.Debug().Msgf(">> Starting Watcher") + a.informer = watch.NewInformer(a.conn(), ns) a.informer.Run(a.stopCh) } -func (a *appView) bailOut() { +// BailOut exists the application. +func (a *appView) BailOut() { if a.stopCh != nil { log.Debug().Msg("<<<< Stopping Watcher") close(a.stopCh) + a.stopCh = nil } if a.cancel != nil { a.cancel() + a.cancel = nil } if a.cancelSkin != nil { a.cancelSkin() + a.cancelSkin = nil } + a.stopForwarders() + a.Stop() +} +func (a *appView) stopForwarders() { for _, f := range a.forwarders { + log.Debug().Msgf("Deleting forwarder %s", f.Path()) f.Stop() } - - a.Stop() + a.forwarders = []forwarder{} } func (a *appView) conn() k8s.Connection { @@ -219,7 +230,7 @@ func (a *appView) stylesUpdater(ctx context.Context) error { // Run starts the application loop func (a *appView) Run() { // Only enable skin updater while in dev mode. - if a.version == devMode { + if a.version == devMode && a.hasSkins { var ctx context.Context ctx, a.cancelSkin = context.WithCancel(context.Background()) if err := a.stylesUpdater(ctx); err != nil { @@ -230,7 +241,7 @@ func (a *appView) Run() { go func() { <-time.After(splashTime * time.Second) a.QueueUpdateDraw(func() { - a.showPage("main") + a.pages.SwitchToPage("main") }) }() @@ -247,6 +258,8 @@ func (a *appView) statusReset() { func (a *appView) status(l flashLevel, msg string) { switch l { + case flashErr: + a.logoView.err(msg) case flashWarn: a.logoView.warn(msg) case flashInfo: @@ -342,7 +355,7 @@ func (a *appView) quitCmd(evt *tcell.EventKey) *tcell.EventKey { if a.cmdMode() { return evt } - a.bailOut() + a.BailOut() return nil } @@ -393,10 +406,6 @@ func (a *appView) gotoResource(res string, record bool) bool { return valid } -func (a *appView) showPage(p string) { - a.pages.SwitchToPage(p) -} - func (a *appView) inject(i igniter) { if a.cancel != nil { a.cancel() @@ -457,11 +466,21 @@ func (a *appView) nextFocus() { return } +func (a *appView) initBench() { + var err error + if a.bench, err = config.NewBench(config.K9sBenchmarkFile); err != nil { + log.Warn().Err(err).Msg("No benchmark config file found, using defaults.") + } +} + func (a *appView) refreshStyles() { var err error if a.styles, err = config.NewStyles(); err != nil { log.Warn().Err(err).Msg("No skin file found. Loading defaults.") } + if err == nil { + a.hasSkins = true + } a.styles.Update() stdColor = config.AsColor(a.styles.Style.Status.NewColor) diff --git a/internal/views/bench.go b/internal/views/bench.go index 2a16e08d..f60b321b 100644 --- a/internal/views/bench.go +++ b/internal/views/bench.go @@ -80,6 +80,7 @@ func (v *benchView) init(ctx context.Context, _ string) { v.refresh() tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+7, true tv.refresh() + tv.Select(1, 0) v.app.SetFocus(tv) } @@ -90,20 +91,6 @@ func (v *benchView) refresh() { v.selChanged(v.selectedRow, 0) } -func (v *benchView) getTV() *tableView { - if vu, ok := v.GetPrimitive("table").(*tableView); ok { - return vu - } - return nil -} - -func (v *benchView) getDetails() *detailsView { - if vu, ok := v.GetPrimitive("details").(*detailsView); ok { - return vu - } - return nil -} - func (v *benchView) registerActions() { v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false) v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false) @@ -119,6 +106,7 @@ func (v *benchView) getTitle() string { } func (v *benchView) selChanged(r, c int) { + log.Info().Msgf("Bench sel changed %d:%d", r, c) tv := v.getTV() if r == 0 || tv.GetCell(r, 0) == nil { v.selectedItem = "" @@ -148,14 +136,13 @@ func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey { if sel == "" { return nil } - - data, err := ioutil.ReadFile(filepath.Join(K9sBenchDir, sel)) + dir := filepath.Join(K9sBenchDir, v.app.config.K9s.CurrentCluster) + data, err := ioutil.ReadFile(filepath.Join(dir, sel)) if err != nil { log.Error().Err(err).Msg("Read failed") v.app.flash().errf("Unable to load bench file %s", err) return nil } - log.Debug().Msgf("Bench %v", string(data)) vu := v.getDetails() vu.Clear() fmt.Fprintln(vu, string(data)) @@ -176,8 +163,9 @@ func (v *benchView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } + dir := filepath.Join(K9sBenchDir, v.app.config.K9s.CurrentCluster) showModal(v.Pages, fmt.Sprintf("Deleting `%s are you sure?", sel), "table", func() { - if err := os.Remove(filepath.Join(K9sBenchDir, sel)); err != nil { + if err := os.Remove(filepath.Join(dir, sel)); err != nil { v.app.flash().errf("Unable to delete file %s", err) log.Error().Err(err).Msg("Delete failed") return @@ -203,22 +191,28 @@ func (v *benchView) hints() hints { } func (v *benchView) hydrate() resource.TableData { - cmds := helpCmds(v.app.conn()) - data := resource.TableData{ - Header: benchHeader, - Rows: make(resource.RowEvents, len(cmds)), + Header: benchHeader, + Rows: make(resource.RowEvents, 10), + NumCols: map[string]bool{ + benchHeader[3]: true, + benchHeader[4]: true, + benchHeader[5]: true, + benchHeader[6]: true, + }, Namespace: resource.AllNamespaces, } - ff, err := ioutil.ReadDir(K9sBenchDir) + dir := filepath.Join(K9sBenchDir, v.app.config.K9s.CurrentCluster) + log.Debug().Msgf("----> DIR %s", dir) + ff, err := ioutil.ReadDir(dir) if err != nil { log.Error().Err(err).Msg("Reading bench dir") v.app.flash().errf("Unable to read bench directory %s", err) } for _, f := range ff { - bench, err := ioutil.ReadFile(filepath.Join(K9sBenchDir, f.Name())) + bench, err := ioutil.ReadFile(filepath.Join(dir, f.Name())) if err != nil { continue } @@ -269,7 +263,7 @@ func augmentRow(fields resource.Row, data string) { sum += m } } - fields[col] = strconv.Itoa(sum) + fields[col] = asNum(sum) } col++ @@ -282,7 +276,7 @@ func augmentRow(fields resource.Row, data string) { sum += m } } - fields[col] = strconv.Itoa(sum) + fields[col] = asNum(sum) } } @@ -305,14 +299,29 @@ func (v *benchView) watchBenchDir(ctx context.Context) error { v.refresh() }) case err := <-w.Errors: - log.Info().Err(err).Msg("Skin watcher failed") + log.Info().Err(err).Msg("Dir Watcher failed") return case <-ctx.Done(): + log.Debug().Msg("!!!! FS WATCHER DONE!!") w.Close() return } } }() - return w.Add(K9sBenchDir) + return w.Add(filepath.Join(K9sBenchDir, v.app.config.K9s.CurrentCluster)) +} + +func (v *benchView) getTV() *tableView { + if vu, ok := v.GetPrimitive("table").(*tableView); ok { + return vu + } + return nil +} + +func (v *benchView) getDetails() *detailsView { + if vu, ok := v.GetPrimitive("details").(*detailsView); ok { + return vu + } + return nil } diff --git a/internal/views/bench_test.go b/internal/views/bench_test.go index f0e21c5b..17109bc8 100644 --- a/internal/views/bench_test.go +++ b/internal/views/bench_test.go @@ -14,19 +14,19 @@ func TestAugmentRow(t *testing.T) { e resource.Row }{ "cool": { - "assets/b1.txt", + "test_assets/b1.txt", resource.Row{"pass", "3.3544", "29.8116", "100", "0"}, }, "2XX": { - "assets/b4.txt", + "test_assets/b4.txt", resource.Row{"pass", "3.3544", "29.8116", "160", "0"}, }, "4XX/5XX": { - "assets/b2.txt", + "test_assets/b2.txt", resource.Row{"pass", "3.3544", "29.8116", "100", "12"}, }, "toast": { - "assets/b3.txt", + "test_assets/b3.txt", resource.Row{"fail", "2.3688", "35.4606", "0", "0"}, }, } diff --git a/internal/views/benchmark.go b/internal/views/benchmark.go index 211d87ce..ad803ed2 100644 --- a/internal/views/benchmark.go +++ b/internal/views/benchmark.go @@ -59,29 +59,33 @@ func (b *benchmark) annuled() bool { } func (b *benchmark) cancel() { + if b == nil { + return + } b.canceled = true b.worker.Stop() } -func (b *benchmark) run(done func()) { +func (b *benchmark) run(cluster string, done func()) { buff := new(bytes.Buffer) b.worker.Writer = buff b.worker.Run() if !b.canceled { - if err := b.save(buff); err != nil { + if err := b.save(cluster, buff); err != nil { log.Error().Err(err).Msg("Saving benchmark") } } done() } -func (b *benchmark) save(r io.Reader) error { - if err := os.MkdirAll(K9sBenchDir, 0777); err != nil { +func (b *benchmark) save(cluster string, r io.Reader) error { + dir := filepath.Join(K9sBenchDir, cluster) + if err := os.MkdirAll(dir, 0744); err != nil { return err } ns, n := namespaced(b.config.Path) - file := filepath.Join(K9sBenchDir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano())) + file := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano())) f, err := os.Create(file) if err != nil { return err diff --git a/internal/views/cluster_info.go b/internal/views/cluster_info.go index fc35df4a..2cd44dcf 100644 --- a/internal/views/cluster_info.go +++ b/internal/views/cluster_info.go @@ -1,6 +1,8 @@ package views import ( + "runtime" + "strconv" "strings" "github.com/derailed/k9s/internal/config" @@ -95,7 +97,7 @@ func (v *clusterInfoView) refresh() { cluster := resource.NewCluster(v.app.conn(), &log.Logger, v.mxs) var row int - v.GetCell(row, 1).SetText(cluster.ContextName()) + v.GetCell(row, 1).SetText(cluster.ContextName() + ":" + strconv.Itoa(runtime.NumGoroutine())) row++ v.GetCell(row, 1).SetText(cluster.ClusterName()) row++ @@ -127,7 +129,7 @@ func (v *clusterInfoView) refresh() { if cpu == "0" { cpu = resource.NAValue } - c.SetText(cpu + deltas(strip(c.Text), cpu)) + c.SetText(cpu + "%" + deltas(strip(c.Text), cpu)) row++ c = v.GetCell(row, 1) @@ -135,7 +137,7 @@ func (v *clusterInfoView) refresh() { if mem == "0" { mem = resource.NAValue } - c.SetText(mem + deltas(strip(c.Text), mem)) + c.SetText(mem + "%" + deltas(strip(c.Text), mem)) } func strip(s string) string { diff --git a/internal/views/cmd_stack.go b/internal/views/cmd_stack.go index f0ea81a3..b3f3ef65 100644 --- a/internal/views/cmd_stack.go +++ b/internal/views/cmd_stack.go @@ -18,7 +18,6 @@ func (s *cmdStack) push(cmd string) { s.stack = s.stack[1 : len(s.stack)-1] } s.stack = append(s.stack, cmd) - log.Info().Msgf("Pushed %s %v", cmd, s.stack) } func (s *cmdStack) pop() (string, bool) { diff --git a/internal/views/command.go b/internal/views/command.go index d7a8bc56..621c98c3 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -32,8 +32,13 @@ func (c *command) pushCmd(cmd string) { } func (c *command) previousCmd() (string, bool) { + if c.lastCmd() { + return c.history.top() + } + c.history.pop() c.app.crumbsView.update(c.history.stack) + return c.history.top() } @@ -52,7 +57,7 @@ func (c *command) run(cmd string) bool { var v resourceViewer switch { case cmd == "q", cmd == "quit": - c.app.bailOut() + c.app.BailOut() return true case cmd == "?", cmd == "help": c.app.inject(newHelpView(c.app)) diff --git a/internal/views/container.go b/internal/views/container.go index 4f57e9f7..fbb80301 100644 --- a/internal/views/container.go +++ b/internal/views/container.go @@ -1,6 +1,7 @@ package views import ( + "context" "errors" "strings" @@ -29,12 +30,29 @@ func newContainerView(t string, app *appView, list resource.List, path string, e v.exitFn = exitFn } v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) - v.switchPage("co") - v.selChanged(1, 0) return &v } +func (v *containerView) init(ctx context.Context, ns string) { + v.resourceView.init(ctx, ns) + // v.selChanged(1, 0) +} + +func (v *containerView) extraActions(aa keyActions) { + aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) + aa[KeyShiftF] = newKeyAction("PortForward", v.portFwdCmd, true) + aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true) + aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) + aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false) + aa[KeyP] = newKeyAction("Previous", v.backCmd, false) + aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false) + aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true) + aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true) + aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(8, false), true) + aa[KeyAltM] = newKeyAction("Sort MEM%", v.sortColCmd(9, false), true) +} + // Protocol... func (v *containerView) backFn() actionHandler { @@ -90,29 +108,16 @@ func (v *containerView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - v.suspend() - { - shellIn(v.app, *v.path, v.selectedItem) - } - v.resume() - + v.stopUpdates() + // v.suspend() + // { + shellIn(v.app, *v.path, v.selectedItem) + // } + // v.resume() + v.restartUpdates() return nil } -func (v *containerView) extraActions(aa keyActions) { - aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) - aa[KeyShiftF] = newKeyAction("PortFwd", v.portFwdCmd, true) - aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true) - aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) - aa[tcell.KeyEscape] = newKeyAction("Back", v.backCmd, false) - aa[KeyP] = newKeyAction("Previous", v.backCmd, false) - aa[tcell.KeyEnter] = newKeyAction("View Logs", v.logsCmd, false) - aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(6, false), true) - aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(7, false), true) - aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(8, false), true) - aa[KeyAltM] = newKeyAction("Sort MEM%", v.sortColCmd(9, false), true) -} - func (v *containerView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { t := v.getTV() @@ -128,15 +133,15 @@ func (v *containerView) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - cell := v.getTV().GetCell(v.selectedRow, 10) - ports := strings.Split(cell.Text, ",") + portC := v.getTV().GetCell(v.selectedRow, 10) + ports := strings.Split(portC.Text, ",") if len(ports) == 0 { - v.app.flash().err(errors.New("No ports to foward to")) + v.app.flash().err(errors.New("Container exposes no ports")) return nil } port := strings.TrimSpace(ports[0]) if port == "" { - v.app.flash().err(errors.New("No ports to foward to")) + v.app.flash().err(errors.New("Container exposed no ports")) return nil } f := tview.NewForm() @@ -158,7 +163,8 @@ func (v *containerView) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { f.AddButton("OK", func() { pf := k8s.NewPortForward(v.app.conn(), &log.Logger) ports := []string{f2 + ":" + f1} - fw, err := pf.Start(*v.path, ports) + co := strings.TrimSpace(v.getTV().GetCell(v.selectedRow, 0).Text) + fw, err := pf.Start(*v.path, co, ports) if err != nil { log.Error().Err(err).Msg("Fort Forward") v.app.flash().errf("PortForward failed! %v", err) @@ -174,8 +180,10 @@ func (v *containerView) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { }) pf.SetActive(true) if err := f.ForwardPorts(); err != nil { - v.app.forwarders = v.app.forwarders[:len(v.app.forwarders)-1] v.app.QueueUpdate(func() { + if len(v.app.forwarders) > 0 { + v.app.forwarders = v.app.forwarders[:len(v.app.forwarders)-1] + } pf.SetActive(false) log.Error().Err(err).Msg("Port forward failed") v.app.flash().errf("PortForward failed %s", err) diff --git a/internal/views/context.go b/internal/views/context.go index 0dc7993f..66682a0b 100644 --- a/internal/views/context.go +++ b/internal/views/context.go @@ -13,15 +13,14 @@ type contextView struct { func newContextView(t string, app *appView, list resource.List) resourceViewer { v := contextView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.getTV().cleanseFn = v.cleanser - v.switchPage("ctx") - } + v.extraActionsFn = v.extraActions + v.getTV().cleanseFn = v.cleanser + return &v } func (v *contextView) extraActions(aa keyActions) { + delete(v.getTV().actions, KeyShiftA) aa[tcell.KeyEnter] = newKeyAction("Switch", v.useCmd, true) } @@ -59,6 +58,7 @@ func (v *contextView) useContext(name string) error { v.app.startInformer() v.app.config.Reset() v.app.config.Save() + v.app.stopForwarders() v.app.flash().infof("Switching context to %s", ctx) v.refresh() if tv, ok := v.GetPrimitive("ctx").(*tableView); ok { diff --git a/internal/views/cronjob.go b/internal/views/cronjob.go index 8a1ece49..71b94e97 100644 --- a/internal/views/cronjob.go +++ b/internal/views/cronjob.go @@ -10,11 +10,8 @@ type cronJobView struct { } func newCronJobView(t string, app *appView, list resource.List) resourceViewer { - v := cronJobView{ - resourceView: newResourceView(t, app, list).(*resourceView), - } + v := cronJobView{resourceView: newResourceView(t, app, list).(*resourceView)} v.extraActionsFn = v.extraActions - v.switchPage("cronjob") return &v } diff --git a/internal/views/dp.go b/internal/views/dp.go index e5085e73..e8dc88be 100644 --- a/internal/views/dp.go +++ b/internal/views/dp.go @@ -15,10 +15,7 @@ type deployView struct { func newDeployView(t string, app *appView, list resource.List) resourceViewer { v := deployView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("deploy") - } + v.extraActionsFn = v.extraActions return &v } @@ -66,6 +63,8 @@ func (v *deployView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *deployView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + // Reset namespace to what it was + v.app.config.SetActiveNamespace(v.list.GetNamespace()) v.app.inject(v) return nil diff --git a/internal/views/ds.go b/internal/views/ds.go index 29aaccac..6e81adb8 100644 --- a/internal/views/ds.go +++ b/internal/views/ds.go @@ -15,10 +15,7 @@ type daemonSetView struct { func newDaemonSetView(t string, app *appView, list resource.List) resourceViewer { v := daemonSetView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("ds") - } + v.extraActionsFn = v.extraActions return &v } @@ -60,12 +57,14 @@ func (v *daemonSetView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { v.app.flash().err(err) return evt } - showPods(v.app, "", "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd) + showPods(v.app, ns, "DaemonSet", v.selectedItem, sel.String(), "", v.backCmd) return nil } func (v *daemonSetView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + // Reset namespace to what it was + v.app.config.SetActiveNamespace(v.list.GetNamespace()) v.app.inject(v) return nil diff --git a/internal/views/forward.go b/internal/views/forward.go index ccca4dfa..a6be6444 100644 --- a/internal/views/forward.go +++ b/internal/views/forward.go @@ -4,12 +4,14 @@ import ( "context" "errors" "fmt" - "runtime" + "path/filepath" "strings" "time" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/resource" "github.com/derailed/tview" + "github.com/fsnotify/fsnotify" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" ) @@ -17,6 +19,7 @@ import ( const ( forwardTitle = "Port Forwards" forwardTitleFmt = " [aqua::b]%s([fuchsia::b]%d[fuchsia::-])[aqua::-] " + promptPage = "prompt" ) type forwardView struct { @@ -48,10 +51,15 @@ func newForwardView(app *appView) *forwardView { } // Init the view. -func (v *forwardView) init(context.Context, string) { +func (v *forwardView) init(ctx context.Context, _ string) { + if err := watchFS(ctx, v.app, config.K9sHome, config.K9sBenchmarks, v.reload); err != nil { + log.Error().Err(err).Msg("Benchdir watch failed!") + v.app.flash().errf("Unable to watch benchmarks directory %s", err) + } + tv := v.getTV() v.refresh() - tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+4, true + tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+6, true tv.refresh() tv.Select(1, 0) v.app.SetFocus(tv) @@ -61,10 +69,17 @@ func (v *forwardView) getTV() *tableView { if vu, ok := v.GetPrimitive("table").(*tableView); ok { return vu } - return nil } +func (v *forwardView) reload() { + if err := v.app.bench.Reload(); err != nil { + log.Error().Err(err).Msg("Bench config reload") + v.app.flash().err(err) + } + v.refresh() +} + func (v *forwardView) refresh() { tv := v.getTV() tv.update(v.hydrate()) @@ -101,8 +116,8 @@ func (v *forwardView) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey { if v.bench != nil { log.Debug().Msg(">>> Benchmark canceled!!") v.app.flash().info("Benchmark canceled!") + v.app.status(flashErr, "Benchmark Camceled!") v.bench.cancel() - v.bench = nil } v.app.statusReset() @@ -114,7 +129,6 @@ func (v *forwardView) benchCmd(evt *tcell.EventKey) *tcell.EventKey { if sel == "" { return nil } - if v.bench != nil { v.app.flash().err(errors.New("Only one benchmark allowed at a time")) return nil @@ -122,31 +136,43 @@ func (v *forwardView) benchCmd(evt *tcell.EventKey) *tcell.EventKey { tv := v.getTV() r, _ := tv.GetSelection() - url := strings.TrimSpace(tv.GetCell(r, 4).Text) - log.Debug().Msgf("Go Routines before %d", runtime.NumGoroutine()) - cfg := benchConfig{ - Method: "GET", - Path: sel, - URL: url, - C: 1, - N: 200, + c, n := v.app.bench.Benchmarks.Defaults.C, v.app.bench.Benchmarks.Defaults.N + m, url := config.DefaultMethod, strings.TrimSpace(tv.GetCell(r, 4).Text) + container := strings.TrimSpace(tv.GetCell(r, 2).Text) + if b, ok := v.app.bench.Benchmarks.Containers[container]; ok { + c, n = b.C, b.N } + + cfg := benchConfig{ + Path: sel, + Method: m, + URL: url, + C: c, + N: n, + } + log.Debug().Msgf(">>>>> BENCHCONFIG %#v", cfg) var err error if v.bench, err = newBenchmark(cfg); err != nil { log.Error().Err(err).Msg("Bench failed!") v.app.flash().errf("Bench failed %v", err) v.app.statusReset() + v.bench = nil return nil } - v.app.status(flashWarn, "Starting Benchmark...") + v.app.status(flashWarn, "Benchmark in progress...") log.Debug().Msg("Bench starting...") - go v.bench.run(func() { + go v.bench.run(v.app.config.K9s.CurrentCluster, func() { log.Debug().Msg("Bench Completed!") v.app.QueueUpdate(func() { + if v.bench.canceled { + v.app.status(flashInfo, "Benchmark canceled") + } else { + v.app.flash().infof("Benchmark for %s is done!", sel) + v.app.status(flashInfo, "Benchmark Completed!") + v.bench.cancel() + } v.bench = nil - v.app.flash().infof("Benchmark for %s is done!", sel) - v.app.status(flashInfo, "Benchmark Completed!") go func() { <-time.After(2 * time.Second) v.app.QueueUpdate(func() { @@ -155,7 +181,6 @@ func (v *forwardView) benchCmd(evt *tcell.EventKey) *tcell.EventKey { }() }) }) - log.Debug().Msgf("Go Routines after %d", runtime.NumGoroutine()) return nil } @@ -176,7 +201,6 @@ func (v *forwardView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { tv.cmdBuff.reset() return nil } - sel := v.getSelectedItem() if sel == "" { return nil @@ -192,11 +216,13 @@ func (v *forwardView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { if index == -1 { return } + v.app.forwarders[index].Stop() if index == 0 && len(v.app.forwarders) == 1 { v.app.forwarders = []forwarder{} } else { v.app.forwarders = append(v.app.forwarders[:index], v.app.forwarders[index+1:]...) } + log.Debug().Msgf("PortForwards after delete: %#v", v.app.forwarders) v.getTV().update(v.hydrate()) v.app.flash().infof("PortForward %s deleted!", sel) }) @@ -219,38 +245,35 @@ func (v *forwardView) backCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (v *forwardView) runCmd(evt *tcell.EventKey) *tcell.EventKey { - tv := v.getTV() - r, _ := tv.GetSelection() - if r > 0 { - v.app.gotoResource(strings.TrimSpace(tv.GetCell(r, 0).Text), true) - } - - return nil -} - func (v *forwardView) hints() hints { return v.getTV().actions.toHints() } func (v *forwardView) hydrate() resource.TableData { - cmds := helpCmds(v.app.conn()) - data := resource.TableData{ - Header: resource.Row{"NAMESPACE", "NAME", "PORTS", "ACTIVE", "URL", "AGE"}, - Rows: make(resource.RowEvents, len(cmds)), + Header: resource.Row{"NAMESPACE", "NAME", "CONTAINER", "PORTS", "URL", "C", "N", "AGE"}, + NumCols: map[string]bool{"C": true, "N": true}, + Rows: make(resource.RowEvents, len(v.app.forwarders)), Namespace: resource.AllNamespaces, } + dc, dn := v.app.bench.Benchmarks.Defaults.C, v.app.bench.Benchmarks.Defaults.N for _, f := range v.app.forwarders { + c, n := dc, dn + if b, ok := v.app.bench.Benchmarks.Containers[f.Container()]; ok { + c, n = b.C, b.N + } + ports := strings.Split(f.Ports()[0], ":") - ns, n := namespaced(f.Path()) + ns, na := namespaced(f.Path()) fields := resource.Row{ ns, - n, + na, + f.Container(), strings.Join(f.Ports(), ","), - fmt.Sprintf("%t", f.Active()), - "http://localhost" + ":" + ports[0], + urlFor(v.app.bench.Benchmarks, f.Container(), ports[0]), + asNum(c), + asNum(n), f.Age(), } data.Rows[f.Path()] = &resource.RowEvent{ @@ -267,7 +290,19 @@ func (v *forwardView) resetTitle() { v.SetTitle(fmt.Sprintf(forwardTitleFmt, forwardTitle, v.getTV().GetRowCount()-1)) } -const genericPrompt = "prompt" +// ---------------------------------------------------------------------------- +// Helpers... + +func urlFor(cfg *config.Benchmarks, co, port string) string { + path := "/" + if b, ok := cfg.Containers[co]; ok { + if b.Path != "" { + path = b.Path + } + } + + return "http://localhost" + ":" + port + path +} func showModal(pv *tview.Pages, msg, back string, ok func()) { m := tview.NewModal(). @@ -281,11 +316,46 @@ func showModal(pv *tview.Pages, msg, back string, ok func()) { dismissModal(pv, back) }) m.SetTitle("") - pv.AddPage(genericPrompt, m, false, false) - pv.ShowPage(genericPrompt) + pv.AddPage(promptPage, m, false, false) + pv.ShowPage(promptPage) } func dismissModal(pv *tview.Pages, page string) { - pv.RemovePage(genericPrompt) + pv.RemovePage(promptPage) pv.SwitchToPage(page) } + +func watchFS(ctx context.Context, app *appView, dir, file string, cb func()) error { + w, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + path := filepath.Join(dir, file) + if file == "" { + path = "" + } + go func() { + for { + select { + case evt := <-w.Events: + log.Debug().Msgf("Event %#v", evt) + if file == "" || evt.Name == path { + log.Debug().Msgf("FS %s event %v", dir, evt) + app.QueueUpdateDraw(func() { + cb() + }) + } + case err := <-w.Errors: + log.Info().Err(err).Msgf("FS %s watcher failed", dir) + return + case <-ctx.Done(): + log.Debug().Msgf("<>", dir) + w.Close() + return + } + } + }() + + return w.Add(dir) +} diff --git a/internal/views/helpers.go b/internal/views/helpers.go index aafd7f5f..453c2c5d 100644 --- a/internal/views/helpers.go +++ b/internal/views/helpers.go @@ -7,6 +7,8 @@ import ( "time" res "github.com/derailed/k9s/internal/resource" + "golang.org/x/text/language" + "golang.org/x/text/message" "k8s.io/apimachinery/pkg/api/resource" ) @@ -107,3 +109,9 @@ func numerical(s string) (int, bool) { return n, true } + +// AsNumb prints a number with thousand separator. +func asNum(n int) string { + p := message.NewPrinter(language.English) + return p.Sprintf("%d", n) +} diff --git a/internal/views/job.go b/internal/views/job.go index dafa5204..41e7f40b 100644 --- a/internal/views/job.go +++ b/internal/views/job.go @@ -15,18 +15,13 @@ type jobView struct { func newJobView(t string, app *appView, list resource.List) resourceViewer { v := jobView{resourceView: newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) - v.switchPage("job") - } + v.extraActionsFn = v.extraActions + v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) picker := newSelectList(&v) - { - picker.setActions(keyActions{ - tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true}, - }) - } + picker.setActions(keyActions{ + tcell.KeyEscape: {description: "Back", action: v.backCmd, visible: true}, + }) v.AddPage("picker", picker, true, false) return &v @@ -34,8 +29,7 @@ func newJobView(t string, app *appView, list resource.List) resourceViewer { // Protocol... -func (v *jobView) setExtraActionsFn(f actionsFn) { -} +func (v *jobView) setExtraActionsFn(f actionsFn) {} func (v *jobView) appView() *appView { return v.app diff --git a/internal/views/logo.go b/internal/views/logo.go index 7b4aef53..8f2e5a5f 100644 --- a/internal/views/logo.go +++ b/internal/views/logo.go @@ -34,10 +34,14 @@ func (v *logoView) reset() { v.refreshLogo(v.app.styles.Style.LogoColor) } -func (v *logoView) warn(msg string) { +func (v *logoView) err(msg string) { v.update(msg, "red") } +func (v *logoView) warn(msg string) { + v.update(msg, "mediumvioletred") +} + func (v *logoView) info(msg string) { v.update(msg, "green") } diff --git a/internal/views/no.go b/internal/views/no.go index 948cdb2e..42f666b9 100644 --- a/internal/views/no.go +++ b/internal/views/no.go @@ -11,10 +11,7 @@ type nodeView struct { func newNodeView(t string, app *appView, list resource.List) resourceViewer { v := nodeView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("no") - } + v.extraActionsFn = v.extraActions return &v } diff --git a/internal/views/ns.go b/internal/views/ns.go index aed7cdb9..6d4f2c9a 100644 --- a/internal/views/ns.go +++ b/internal/views/ns.go @@ -22,13 +22,10 @@ type namespaceView struct { func newNamespaceView(t string, app *appView, list resource.List) resourceViewer { v := namespaceView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.selectedFn = v.getSelectedItem - v.decorateFn = v.decorate - v.getTV().cleanseFn = v.cleanser - v.switchPage("ns") - } + v.extraActionsFn = v.extraActions + v.selectedFn = v.getSelectedItem + v.decorateFn = v.decorate + v.getTV().cleanseFn = v.cleanser return &v } diff --git a/internal/views/pod.go b/internal/views/pod.go index 577957a9..8c9adb06 100644 --- a/internal/views/pod.go +++ b/internal/views/pod.go @@ -18,9 +18,11 @@ const containerFmt = "[fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])" type podView struct { *resourceView - cancel context.CancelFunc + childCancelFn context.CancelFunc } +var _ updatable = &podView{} + type loggable interface { appView() *appView backFn() actionHandler @@ -43,15 +45,29 @@ func newPodView(t string, app *appView, list resource.List) resourceViewer { } v.AddPage("picker", picker, true, false) v.AddPage("logs", newLogsView(list.GetName(), &v), true, false) - v.switchPage("po") return &v } +func (v *podView) extraActions(aa keyActions) { + aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) + aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true) + aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) + aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true) + aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true) + aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true) + aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true) + aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true) + aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(6, false), true) + aa[KeyAltM] = newKeyAction("Sort MEM%", v.sortColCmd(7, false), true) + aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(8, true), true) +} + func (v *podView) listContainers(app *appView, _, res, sel string) { if !v.rowSelected() { return } + po, err := v.app.informer.Get(watch.PodIndex, sel, metav1.GetOptions{}) if err != nil { log.Error().Err(err).Msgf("Unable to retrieve pod %s", sel) @@ -63,19 +79,26 @@ func (v *podView) listContainers(app *appView, _, res, sel string) { list := resource.NewContainerList(app.conn(), mx, pod) title := skinTitle(fmt.Sprintf(containerFmt, "Containers", sel), v.app.styles.Style) - v.suspend() + // Stop my updater + if v.cancelFn != nil { + v.cancelFn() + } + + // Span child view cv := newContainerView(title, app, list, fqn(pod.Namespace, pod.Name), v.exitFn) v.AddPage("containers", cv, true, true) - ctx, cancel := context.WithCancel(v.context) - v.cancel = cancel + var ctx context.Context + ctx, v.childCancelFn = context.WithCancel(v.parentCtx) cv.init(ctx, pod.Namespace) } func (v *podView) exitFn() { - v.cancel() - v.switchPage("po") + if v.childCancelFn != nil { + v.childCancelFn() + } v.RemovePage("containers") - v.resume() + v.switchPage("po") + v.restartUpdates() } // Protocol... @@ -102,7 +125,6 @@ func (v *podView) logsCmd(evt *tcell.EventKey) *tcell.EventKey { if v.viewLogs(false) { return nil } - return evt } @@ -110,7 +132,6 @@ func (v *podView) prevLogsCmd(evt *tcell.EventKey) *tcell.EventKey { if v.viewLogs(true) { return nil } - return evt } @@ -118,12 +139,14 @@ func (v *podView) viewLogs(prev bool) bool { if !v.rowSelected() { return false } + cc, err := fetchContainers(v.list, v.selectedItem, true) if err != nil { v.app.flash().errf("Unable to retrieve containers %s", err) log.Error().Err(err) return false } + if len(cc) == 1 { v.showLogs(v.selectedItem, cc[0], v.list.GetName(), v, prev) return true @@ -148,6 +171,7 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { if !v.rowSelected() { return evt } + cc, err := fetchContainers(v.list, v.selectedItem, false) if err != nil { v.app.flash().errf("Unable to retrieve containers %s", err) @@ -169,11 +193,29 @@ func (v *podView) shellCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *podView) shellIn(path, co string) { - v.suspend() - { - shellIn(v.app, path, co) + v.stopUpdates() + shellIn(v.app, path, co) + v.restartUpdates() +} + +func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + t := v.getTV() + t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc + t.refresh() + + return nil } - v.resume() +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) { + if len(po) == 0 { + return []string{}, nil + } + return l.Resource().(resource.Containers).Containers(po, includeInit) } func shellIn(a *appView, path, co string) { @@ -199,34 +241,3 @@ func computeShellArgs(path, co, context string, cfg *string) []string { return a } - -func (v *podView) extraActions(aa keyActions) { - aa[KeyL] = newKeyAction("Logs", v.logsCmd, true) - aa[KeyShiftL] = newKeyAction("Logs Previous", v.prevLogsCmd, true) - aa[KeyS] = newKeyAction("Shell", v.shellCmd, true) - aa[KeyShiftR] = newKeyAction("Sort Ready", v.sortColCmd(1, false), true) - aa[KeyShiftS] = newKeyAction("Sort Status", v.sortColCmd(2, true), true) - aa[KeyShiftT] = newKeyAction("Sort Restart", v.sortColCmd(3, false), true) - aa[KeyShiftC] = newKeyAction("Sort CPU", v.sortColCmd(4, false), true) - aa[KeyShiftM] = newKeyAction("Sort MEM", v.sortColCmd(5, false), true) - aa[KeyAltC] = newKeyAction("Sort CPU%", v.sortColCmd(6, false), true) - aa[KeyAltM] = newKeyAction("Sort MEM%", v.sortColCmd(7, false), true) - aa[KeyShiftO] = newKeyAction("Sort Node", v.sortColCmd(8, true), true) -} - -func (v *podView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - t := v.getTV() - t.sortCol.index, t.sortCol.asc = t.nameColIndex()+col, asc - t.refresh() - - return nil - } -} - -func fetchContainers(l resource.List, po string, includeInit bool) ([]string, error) { - if len(po) == 0 { - return []string{}, nil - } - return l.Resource().(resource.Containers).Containers(po, includeInit) -} diff --git a/internal/views/rbac.go b/internal/views/rbac.go index a61eaca5..400c0cd4 100644 --- a/internal/views/rbac.go +++ b/internal/views/rbac.go @@ -105,8 +105,9 @@ func (v *rbacView) init(c context.Context, ns string) { log.Debug().Msg("RBAC Watch bailing out!") return case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second): - v.refresh() - v.app.Draw() + v.app.QueueUpdateDraw(func() { + v.refresh() + }) } } }(ctx) diff --git a/internal/views/resource.go b/internal/views/resource.go index 5461c974..44e3b563 100644 --- a/internal/views/resource.go +++ b/internal/views/resource.go @@ -22,6 +22,12 @@ const ( clusterRefresh = time.Duration(15 * time.Second) ) +type updatable interface { + restartUpdates() + stopUpdates() + update(context.Context) +} + type resourceView struct { *tview.Pages @@ -39,10 +45,11 @@ type resourceView struct { colorerFn colorerFn actions keyActions mx sync.Mutex - suspended bool - nsListAccess bool - path *string - context context.Context + // suspended bool + nsListAccess bool + path *string + cancelFn context.CancelFunc + parentCtx context.Context } func newResourceView(title string, app *appView, list resource.List) resourceViewer { @@ -56,9 +63,7 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie } tv := newTableView(app, v.title) - { - tv.SetSelectionChangedFunc(v.selChanged) - } + tv.SetSelectionChangedFunc(v.selChanged) v.AddPage(v.list.GetName(), tv, true, true) details := newDetailsView(app, v.backCmd) @@ -67,9 +72,30 @@ func newResourceView(title string, app *appView, list resource.List) resourceVie return &v } +func (v *resourceView) stopUpdates() { + if v.cancelFn != nil { + log.Debug().Msgf(">>> STOP updates %s", v.list.GetName()) + v.cancelFn() + } +} + +func (v *resourceView) restartUpdates() { + log.Debug().Msgf(">>> RESTART updates %s", v.list.GetName()) + if v.cancelFn != nil { + v.cancelFn() + } + + var vctx context.Context + vctx, v.cancelFn = context.WithCancel(v.parentCtx) + v.update(vctx) +} + // Init watches all running pods in given namespace func (v *resourceView) init(ctx context.Context, ns string) { - v.context, v.selectedItem, v.selectedNS = ctx, noSelection, ns + v.parentCtx = ctx + var vctx context.Context + vctx, v.cancelFn = context.WithCancel(ctx) + v.selectedItem, v.selectedNS = noSelection, ns colorer := defaultColorer if v.colorerFn != nil { @@ -92,25 +118,15 @@ func (v *resourceView) init(ctx context.Context, ns string) { } } - go v.updater(ctx) + v.update(vctx) v.app.clusterInfoView.refresh() v.refresh() if tv, ok := v.CurrentPage().Item.(*tableView); ok { tv.Select(1, 0) - v.selChanged(1, 0) } } -func (v *resourceView) reloadList(list resource.List, ns string) { - v.suspend() - { - v.list = list - v.list.SetNamespace(ns) - } - v.resume() -} - -func (v *resourceView) updater(ctx context.Context) { +func (v *resourceView) update(ctx context.Context) { go func(ctx context.Context) { for { select { @@ -118,9 +134,6 @@ func (v *resourceView) updater(ctx context.Context) { log.Debug().Msgf("%s cluster updater canceled!", v.list.GetName()) return case <-time.After(clusterRefresh): - if v.isSuspended() { - continue - } v.app.QueueUpdateDraw(func() { v.app.clusterInfoView.refresh() }) @@ -135,10 +148,8 @@ func (v *resourceView) updater(ctx context.Context) { log.Debug().Msgf("%s updater canceled!", v.list.GetName()) return case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second): - if v.isSuspended() { - continue - } v.app.QueueUpdateDraw(func() { + log.Debug().Msgf(">>> Refreshing %s", v.list.GetName()) v.refresh() }) } @@ -146,33 +157,6 @@ func (v *resourceView) updater(ctx context.Context) { }(ctx) } -func (v *resourceView) isSuspended() bool { - var suspended bool - v.mx.Lock() - { - suspended = v.suspended - } - v.mx.Unlock() - - return suspended -} - -func (v *resourceView) suspend() { - v.mx.Lock() - { - v.suspended = true - } - v.mx.Unlock() -} - -func (v *resourceView) resume() { - v.mx.Lock() - { - v.suspended = false - } - v.mx.Unlock() -} - func (v *resourceView) setExtraActionsFn(f actionsFn) { f(v.actions) } @@ -324,7 +308,7 @@ func (v *resourceView) viewCmd(evt *tcell.EventKey) *tcell.EventKey { } details := v.GetPrimitive("details").(*detailsView) { - details.setCategory("View") + details.setCategory("YAML") details.setTitle(sel) details.SetTextColor(tcell.ColorMediumAquamarine) details.SetText(colorizeYAML(v.app.styles.Style, raw)) @@ -340,14 +324,18 @@ func (v *resourceView) editCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - ns, po := namespaced(v.selectedItem) - args := make([]string, 0, 10) - args = append(args, "edit") - args = append(args, v.list.GetName()) - args = append(args, "-n", ns) - args = append(args, "--context", v.app.config.K9s.CurrentContext) - args = append(args, po) - runK(true, v.app, args...) + v.stopUpdates() + { + ns, po := namespaced(v.selectedItem) + args := make([]string, 0, 10) + args = append(args, "edit") + args = append(args, v.list.GetName()) + args = append(args, "-n", ns) + args = append(args, "--context", v.app.config.K9s.CurrentContext) + args = append(args, po) + runK(true, v.app, args...) + } + v.restartUpdates() return evt } @@ -388,8 +376,8 @@ func (v *resourceView) refresh() { v.list.SetNamespace(v.selectedNS) } if err := v.list.Reconcile(v.app.informer, v.path); err != nil { - log.Error().Err(err).Msg("Reconciliation failed") - v.app.flash().errf("Reconciliation failed %s", err) + log.Error().Err(err).Msgf("Reconciliation for %s failed", v.title) + v.app.flash().errf("Reconciliation for %s failed - %s", v.title, err) } data := v.list.Data() if v.decorateFn != nil { @@ -425,19 +413,21 @@ func (v *resourceView) selectItem(r, c int) { } func (v *resourceView) switchPage(p string) { + log.Debug().Msgf("Switching page to %s", p) if _, ok := v.CurrentPage().Item.(*tableView); ok { - v.suspend() + v.stopUpdates() + } else { + log.Debug().Msgf("Not a table %T", v.CurrentPage().Item) } v.SwitchToPage(p) v.selectedNS = v.list.GetNamespace() - if h, ok := v.GetPrimitive(p).(hinter); ok { - v.app.setHints(h.hints()) + if vu, ok := v.GetPrimitive(p).(hinter); ok { + v.app.setHints(vu.hints()) } - log.Info().Msgf("Current page %#v", v.CurrentPage()) if _, ok := v.CurrentPage().Item.(*tableView); ok { - v.resume() + v.restartUpdates() } } diff --git a/internal/views/rs.go b/internal/views/rs.go index 77de827b..7438c17f 100644 --- a/internal/views/rs.go +++ b/internal/views/rs.go @@ -22,10 +22,7 @@ type replicaSetView struct { func newReplicaSetView(t string, app *appView, list resource.List) resourceViewer { v := replicaSetView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("rs") - } + v.extraActionsFn = v.extraActions return &v } diff --git a/internal/views/secret.go b/internal/views/secret.go index 10794fd9..b9917418 100644 --- a/internal/views/secret.go +++ b/internal/views/secret.go @@ -15,10 +15,7 @@ type secretView struct { func newSecretView(t string, app *appView, list resource.List) resourceViewer { v := secretView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("secret") - } + v.extraActionsFn = v.extraActions return &v } diff --git a/internal/views/sorter.go b/internal/views/sorter.go index 3aec9e62..380d07c5 100644 --- a/internal/views/sorter.go +++ b/internal/views/sorter.go @@ -78,7 +78,7 @@ func isDurationSort(asc bool, c1, c2 string) (bool, bool) { } if asc { - return d1 < d2, true + return d1 <= d2, true } return d1 > d2, true } @@ -103,7 +103,7 @@ func isIntegerSort(asc bool, c1, c2 string) (bool, bool) { } n2, _ := strconv.Atoi(c2) if asc { - return n1 < n2, true + return n1 <= n2, true } return n1 > n2, true } diff --git a/internal/views/sorter_test.go b/internal/views/sorter_test.go index fd68267b..0d355aaf 100644 --- a/internal/views/sorter_test.go +++ b/internal/views/sorter_test.go @@ -10,10 +10,11 @@ import ( func TestGroupSort(t *testing.T) { uu := []struct { - order bool + asc bool rows []string expect []string }{ + {true, []string{"200m", "100m"}, []string{"100m", "200m"}}, {true, []string{"200m", "100m"}, []string{"100m", "200m"}}, {false, []string{"200m", "100m"}, []string{"200m", "100m"}}, {true, []string{"10", "1"}, []string{"1", "10"}}, @@ -31,10 +32,11 @@ func TestGroupSort(t *testing.T) { {true, []string{"95m", "1h30m"}, []string{"1h30m", "95m"}}, {true, []string{"b-21", "b-2"}, []string{"b-2", "b-21"}}, {false, []string{"b-21", "b-2"}, []string{"b-21", "b-2"}}, + {true, []string{"4m", "3m2s"}, []string{"3m2s", "4m"}}, } for _, u := range uu { - g := groupSorter{rows: u.rows, asc: u.order} + g := groupSorter{rows: u.rows, asc: u.asc} sort.Sort(g) assert.Equal(t, u.expect, g.rows) } @@ -42,7 +44,7 @@ func TestGroupSort(t *testing.T) { func TestRowSort(t *testing.T) { uu := []struct { - order bool + asc bool rows, expect resource.Rows }{ { @@ -65,10 +67,15 @@ func TestRowSort(t *testing.T) { resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, resource.Rows{resource.Row{"200Mi"}, resource.Row{"100Mi"}}, }, + { + true, + resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}}, + resource.Rows{resource.Row{"8m4s"}, resource.Row{"31m"}}, + }, } for _, u := range uu { - r := rowSorter{index: 0, rows: u.rows, asc: u.order} + r := rowSorter{index: 0, rows: u.rows, asc: u.asc} sort.Sort(r) assert.Equal(t, u.expect, r.rows) } diff --git a/internal/views/sts.go b/internal/views/sts.go index 40e9b3f1..dc8e18d5 100644 --- a/internal/views/sts.go +++ b/internal/views/sts.go @@ -15,10 +15,7 @@ type statefulSetView struct { func newStatefulSetView(t string, app *appView, list resource.List) resourceViewer { v := statefulSetView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("sts") - } + v.extraActionsFn = v.extraActions return &v } diff --git a/internal/views/svc.go b/internal/views/svc.go index 215ed68a..ff75c7a4 100644 --- a/internal/views/svc.go +++ b/internal/views/svc.go @@ -17,10 +17,7 @@ type svcView struct { func newSvcView(t string, app *appView, list resource.List) resourceViewer { v := svcView{newResourceView(t, app, list).(*resourceView)} - { - v.extraActionsFn = v.extraActions - v.switchPage("svc") - } + v.extraActionsFn = v.extraActions return &v } @@ -52,14 +49,16 @@ func (v *svcView) showPodsCmd(evt *tcell.EventKey) *tcell.EventKey { log.Error().Err(err).Msgf("Fetch service %s", v.selectedItem) return nil } - svc := res.(*v1.Service) - - v.showSvcPods(ns, svc.Spec.Selector, v.backCmd) + if svc, ok := res.(*v1.Service); ok { + v.showSvcPods(ns, svc.Spec.Selector, v.backCmd) + } return nil } func (v *svcView) backCmd(evt *tcell.EventKey) *tcell.EventKey { + // Reset namespace to what it was + v.app.config.SetActiveNamespace(v.list.GetNamespace()) v.app.inject(v) return nil @@ -78,7 +77,7 @@ func (v *svcView) showSvcPods(ns string, sel map[string]string, b actionHandler) pv.setExtraActionsFn(func(aa keyActions) { aa[tcell.KeyEsc] = newKeyAction("Back", b, true) }) - // Reset active namespace to all. + // set active namespace to service ns. v.app.config.SetActiveNamespace(ns) v.app.inject(pv) } diff --git a/internal/views/table.go b/internal/views/table.go index bde3d0e9..06f54eb3 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -23,8 +23,8 @@ const ( ) var ( - crx = regexp.MustCompile(`\A.{0,1}CPU`) - mrx = regexp.MustCompile(`\A.{0,1}MEM`) + cpuRX = regexp.MustCompile(`\A.{0,1}CPU`) + memRX = regexp.MustCompile(`\A.{0,1}MEM`) ) type ( @@ -319,7 +319,7 @@ func (v *tableView) doUpdate(data resource.TableData) { fg := config.AsColor(v.app.styles.Style.Table.Header.FgColor) bg := config.AsColor(v.app.styles.Style.Table.Header.BgColor) for col, h := range data.Header { - v.addHeaderCell(col, h, fg, bg) + v.addHeaderCell(data.NumCols, col, h, fg, bg) } row++ @@ -335,7 +335,7 @@ func (v *tableView) doUpdate(data resource.TableData) { fgColor = v.colorerFn(data.Namespace, data.Rows[sk]) } for col, field := range data.Rows[sk].Fields { - v.addBodyCell(data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads) + v.addBodyCell(data.NumCols, data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads) } row++ } @@ -363,12 +363,12 @@ func (v *tableView) sortAllRows(rows resource.RowEvents, sortFn sortFn) (resourc return prim, sec } -func (v *tableView) addHeaderCell(col int, name string, fg, bg tcell.Color) { +func (v *tableView) addHeaderCell(numCols map[string]bool, col int, name string, fg, bg tcell.Color) { c := tview.NewTableCell(v.sortIndicator(col, name)) { c.SetExpansion(1) c.SetTextColor(fg) - if crx.MatchString(name) || mrx.MatchString(name) { + if numCols[name] || cpuRX.MatchString(name) || memRX.MatchString(name) { c.SetAlign(tview.AlignRight) } c.SetBackgroundColor(bg) @@ -376,10 +376,10 @@ func (v *tableView) addHeaderCell(col int, name string, fg, bg tcell.Color) { v.SetCell(0, col, c) } -func (v *tableView) addBodyCell(header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) { +func (v *tableView) addBodyCell(numCols map[string]bool, header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) { field += deltas(delta, field) align := tview.AlignLeft - if crx.MatchString(header) || mrx.MatchString(header) { + if numCols[header] || cpuRX.MatchString(header) || memRX.MatchString(header) { align = tview.AlignRight } else if isASCII(field) { field = pad(field, pads[col]) diff --git a/internal/views/assets/b1.txt b/internal/views/test_assets/b1.txt similarity index 100% rename from internal/views/assets/b1.txt rename to internal/views/test_assets/b1.txt diff --git a/internal/views/assets/b2.txt b/internal/views/test_assets/b2.txt similarity index 100% rename from internal/views/assets/b2.txt rename to internal/views/test_assets/b2.txt diff --git a/internal/views/assets/b3.txt b/internal/views/test_assets/b3.txt similarity index 100% rename from internal/views/assets/b3.txt rename to internal/views/test_assets/b3.txt diff --git a/internal/views/assets/b4.txt b/internal/views/test_assets/b4.txt similarity index 100% rename from internal/views/assets/b4.txt rename to internal/views/test_assets/b4.txt diff --git a/internal/watch/container_test.go b/internal/watch/container_test.go index 81998d11..ce5180c6 100644 --- a/internal/watch/container_test.go +++ b/internal/watch/container_test.go @@ -3,29 +3,72 @@ package watch import ( "testing" + "github.com/derailed/k9s/internal/k8s" "gotest.tools/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + // "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" ) func TestContainerGet(t *testing.T) { cmo := NewMockConnection() - c := NewContainer(NewPod(cmo, "")) - o, err := c.Get("fred", metav1.GetOptions{}) + assert.ErrorContains(t, err, "not found") assert.Assert(t, o == nil) } func TestContainerList(t *testing.T) { cmo := NewMockConnection() - c := NewContainer(NewPod(cmo, "")) - o := c.List("fred", metav1.ListOptions{}) + assert.Assert(t, o == nil) } +func TestToContainer(t *testing.T) { + c := make(k8s.Collection, 2) + toContainers(makeCoPod("p1"), c) + + assert.Equal(t, 2, len(c)) +} + // ---------------------------------------------------------------------------- // Helpers... + +func makePod(n string) *v1.Pod { + po := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + Namespace: "default", + }, + } + po.Status.Phase = v1.PodRunning + + return po +} + +func makeCoPod(n string) *v1.Pod { + po := makePod(n) + po.Spec = v1.PodSpec{ + InitContainers: []v1.Container{ + makeContainer("i1", "fred:0.0.1"), + }, + Containers: []v1.Container{ + makeContainer("c1", "blee:0.1.0"), + }, + } + + return po +} + +func makeContainer(n, img string) v1.Container { + co := v1.Container{ + Name: n, + Image: img, + } + + return co +} diff --git a/internal/watch/helper_test.go b/internal/watch/helper_test.go index 0d7c56bd..fc60c4c5 100644 --- a/internal/watch/helper_test.go +++ b/internal/watch/helper_test.go @@ -17,6 +17,7 @@ func TestMetaFQN(t *testing.T) { e string }{ "full": {metav1.ObjectMeta{Namespace: "fred", Name: "blee"}, "fred/blee"}, + "nons": {metav1.ObjectMeta{Name: "blee"}, "blee"}, } for k, v := range uu { @@ -45,6 +46,98 @@ func TestMxResourceDiff(t *testing.T) { } } +func TestToSelector(t *testing.T) { + uu := map[string]struct { + s string + e map[string]string + }{ + "cool": { + "app=fred,env=test", + map[string]string{"app": "fred", "env": "test"}, + }, + "empty": { + "", + map[string]string{}, + }, + "hosed": { + "app|blee", + map[string]string{}, + }, + "toast": { + "app,blee", + map[string]string{}, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + m := toSelector(u.s) + for k, v := range m { + assert.Equal(t, u.e[k], v) + } + }) + } +} + +func TestMatchesNode(t *testing.T) { + uu := map[string]struct { + n string + s map[string]string + e bool + }{ + "cool": { + "n1", + map[string]string{"spec.nodeName": "n1"}, + true, + }, + "nomatch": { + "n2", + map[string]string{"spec.nodeName": "n1"}, + false, + }, + "matchAll": { + "n2", + map[string]string{}, + true, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, matchesNode(u.n, u.s)) + }) + } +} + +func TestMatchesLabels(t *testing.T) { + uu := map[string]struct { + l, s map[string]string + e bool + }{ + "cool": { + map[string]string{"spec.nodeName": "n1"}, + map[string]string{"spec.nodeName": "n1"}, + true, + }, + "nomatch": { + map[string]string{"spec.nodeName": "n2"}, + map[string]string{"spec.nodeName": "n1"}, + false, + }, + "matchAll": { + map[string]string{"spec.nodeName": "n2"}, + map[string]string{}, + true, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, matchesLabels(u.l, u.s)) + }) + } +} + // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/watch/helpers.go b/internal/watch/helpers.go index 6b567534..b9e7c12b 100644 --- a/internal/watch/helpers.go +++ b/internal/watch/helpers.go @@ -1,6 +1,7 @@ package watch import ( + "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -24,3 +25,44 @@ func MetaFQN(m metav1.ObjectMeta) string { return m.Namespace + "/" + m.Name } + +// ToSelector converts a string selector into a map. +func toSelector(s string) map[string]string { + var m map[string]string + ls, err := metav1.ParseToLabelSelector(s) + if err != nil { + log.Error().Err(err).Msg("StringToSel") + return m + } + mSel, err := metav1.LabelSelectorAsMap(ls) + if err != nil { + log.Error().Err(err).Msg("SelToMap") + return m + } + + return mSel +} + +// MatchesNode checks if pod selector matches node name. +func matchesNode(name string, selector map[string]string) bool { + if len(selector) == 0 { + return true + } + + return selector["spec.nodeName"] == name +} + +// MatchesLabels check if pod labels matches a given selector. +func matchesLabels(labels, selector map[string]string) bool { + if len(selector) == 0 { + return true + } + for k, v := range selector { + la, ok := labels[k] + if !ok || la != v { + return false + } + } + + return true +} diff --git a/internal/watch/meta.go b/internal/watch/informer.go similarity index 51% rename from internal/watch/meta.go rename to internal/watch/informer.go index b1dd1ddd..e3bd95bc 100644 --- a/internal/watch/meta.go +++ b/internal/watch/informer.go @@ -1,7 +1,9 @@ package watch import ( + "errors" "fmt" + "sync" "github.com/derailed/k9s/internal/k8s" "github.com/rs/zerolog/log" @@ -46,73 +48,67 @@ type StoreInformer interface { List(ns string, opts metav1.ListOptions) k8s.Collection } -// Meta represents a collection of cluster wide watchers. -type Meta struct { +// Informer represents a collection of cluster wide watchers. +type Informer struct { informers map[string]StoreInformer client k8s.Connection podInformer *Pod listenerFn TableListenerFn + initOnce sync.Once } -// NewMeta creates a new cluster resource informer -func NewMeta(client k8s.Connection, ns string) *Meta { - m := Meta{client: client, informers: map[string]StoreInformer{}} +// NewInformer creates a new cluster resource informer +func NewInformer(client k8s.Connection, ns string) *Informer { + log.Debug().Msgf(">> Starting Informer") + i := Informer{client: client, informers: map[string]StoreInformer{}} - nsAccess := m.client.CanIAccess("", "", "namespaces", []string{"list", "watch"}) + nsAccess := i.client.CanIAccess("", "", "namespaces", []string{"list", "watch"}) ns, err := client.Config().CurrentNamespaceName() // User did not lock NS. Check all ns access if not bail if err != nil && !nsAccess { log.Panic().Msg("Unauthorized access to list namespaces. Please specify a namespace") } - // Namespace is locks in check if user has auth for this ns access. + // Namespace is locked in. check if user has auth for this ns access. if ns != AllNamespaces && !nsAccess { - if !m.client.CanIAccess("", ns, "namespaces", []string{"get", "watch"}) { + if !i.client.CanIAccess("", ns, "namespaces", []string{"get", "watch"}) { log.Panic().Msgf("Unauthorized access to namespace %q", ns) } - m.init(ns) + i.init(ns) } else { - m.init(AllNamespaces) + i.init(AllNamespaces) } - return &m + return &i } -func (m *Meta) init(ns string) { - po := NewPod(m.client, ns) - - m.informers = map[string]StoreInformer{ - NodeIndex: NewNode(m.client), - PodIndex: po, - ContainerIndex: NewContainer(po), - } - - if m.client.HasMetrics() { - if m.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}) { - m.informers[NodeMXIndex] = NewNodeMetrics(m.client) - m.informers[PodMXIndex] = NewPodMetrics(m.client, ns) +func (i *Informer) init(ns string) { + i.initOnce.Do(func() { + po := NewPod(i.client, ns) + i.informers = map[string]StoreInformer{ + NodeIndex: NewNode(i.client), + PodIndex: po, + ContainerIndex: NewContainer(po), } - } -} -// CheckAccess checks if current user as enought RBAC fu to access watched resources. -func (m *Meta) checkAccess(ns string) error { - if !m.client.CanIAccess(ns, "nodes", "nodes", []string{"list", "watch"}) { - return fmt.Errorf("Not authorized to list/watch nodes") - } - if !m.client.CanIAccess(ns, "pods", "pods", []string{"list", "watch"}) { - return fmt.Errorf("Not authorized to list/watch pods in namespace %s", ns) - } + if !i.client.HasMetrics() { + return + } - return nil + if i.client.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"}) { + i.informers[NodeMXIndex] = NewNodeMetrics(i.client) + i.informers[PodMXIndex] = NewPodMetrics(i.client, ns) + } + }) } // List items from store. -func (m *Meta) List(res, ns string, opts metav1.ListOptions) (k8s.Collection, error) { - if m == nil { - return nil, fmt.Errorf("No meta exists") +func (i *Informer) List(res, ns string, opts metav1.ListOptions) (k8s.Collection, error) { + if i == nil { + return nil, errors.New("Invalid informer") } - if i, ok := m.informers[res]; ok { + + if i, ok := i.informers[res]; ok { return i.List(ns, opts), nil } @@ -120,8 +116,8 @@ func (m *Meta) List(res, ns string, opts metav1.ListOptions) (k8s.Collection, er } // Get a resource by name. -func (m Meta) Get(res, fqn string, opts metav1.GetOptions) (interface{}, error) { - if informer, ok := m.informers[res]; ok { +func (i Informer) Get(res, fqn string, opts metav1.GetOptions) (interface{}, error) { + if informer, ok := i.informers[res]; ok { return informer.Get(fqn, opts) } @@ -129,10 +125,10 @@ func (m Meta) Get(res, fqn string, opts metav1.GetOptions) (interface{}, error) } // Run starts watching cluster resources. -func (m *Meta) Run(closeCh <-chan struct{}) { - for i := range m.informers { - go func(informer StoreInformer, c <-chan struct{}) { - informer.Run(c) - }(m.informers[i], closeCh) +func (i *Informer) Run(closeCh <-chan struct{}) { + for name := range i.informers { + go func(si StoreInformer, c <-chan struct{}) { + si.Run(c) + }(i.informers[name], closeCh) } } diff --git a/internal/watch/informer_test.go b/internal/watch/informer_test.go new file mode 100644 index 00000000..a73cb1b3 --- /dev/null +++ b/internal/watch/informer_test.go @@ -0,0 +1,91 @@ +package watch + +import ( + "sync" + "testing" + + "github.com/derailed/k9s/internal/k8s" + m "github.com/petergtz/pegomock" + "gotest.tools/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func TestInformerInitWithNS(t *testing.T) { + ns := "ns1" + + f := new(genericclioptions.ConfigFlags) + f.Namespace = &ns + cmo := NewMockConnection() + m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) + m.When(cmo.HasMetrics()).ThenReturn(true) + m.When(cmo.CanIAccess("", "", "namespaces", []string{"list", "watch"})).ThenReturn(false) + m.When(cmo.CanIAccess("", ns, "namespaces", []string{"get", "watch"})).ThenReturn(true) + m.When(cmo.CanIAccess("", ns, "metrics.k8s.io", []string{"list", "watch"})).ThenReturn(true) + i := NewInformer(cmo, ns) + + o, err := i.List(PodIndex, "fred", metav1.ListOptions{}) + assert.NilError(t, err) + assert.Assert(t, len(o) == 0) +} + +func TestInformerList(t *testing.T) { + f := new(genericclioptions.ConfigFlags) + cmo := NewMockConnection() + m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) + i := NewInformer(cmo, "") + + o, err := i.List(PodIndex, "fred", metav1.ListOptions{}) + assert.NilError(t, err) + assert.Assert(t, len(o) == 0) +} + +func TestInformerListNoRes(t *testing.T) { + f := new(genericclioptions.ConfigFlags) + cmo := NewMockConnection() + m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) + i := NewInformer(cmo, "") + + o, err := i.List("dp", "fred", metav1.ListOptions{}) + assert.ErrorContains(t, err, "No informer found") + assert.Assert(t, len(o) == 0) +} + +func TestInformerGet(t *testing.T) { + f := new(genericclioptions.ConfigFlags) + cmo := NewMockConnection() + m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) + i := NewInformer(cmo, "") + + o, err := i.Get(PodIndex, "fred", metav1.GetOptions{}) + assert.ErrorContains(t, err, "Pod fred not found") + assert.Assert(t, o == nil) +} + +func TestInformerGetNoRes(t *testing.T) { + f := new(genericclioptions.ConfigFlags) + cmo := NewMockConnection() + m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) + i := NewInformer(cmo, "") + + o, err := i.Get("rs", "fred", metav1.GetOptions{}) + assert.ErrorContains(t, err, "No informer found") + assert.Assert(t, o == nil) +} + +func TestInformerRun(t *testing.T) { + f := new(genericclioptions.ConfigFlags) + cmo := NewMockConnection() + m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) + i := NewInformer(cmo, "") + + var wg sync.WaitGroup + wg.Add(1) + c := make(chan struct{}) + go func() { + defer wg.Done() + i.Run(c) + }() + close(c) + wg.Wait() +} diff --git a/internal/watch/meta_test.go b/internal/watch/meta_test.go deleted file mode 100644 index f0944b14..00000000 --- a/internal/watch/meta_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package watch - -import ( - "testing" - - "github.com/derailed/k9s/internal/k8s" - m "github.com/petergtz/pegomock" - "gotest.tools/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -func TestMetaList(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - meta := NewMeta(cmo, "") - - o, err := meta.List(PodIndex, "fred", metav1.ListOptions{}) - assert.NilError(t, err) - assert.Assert(t, len(o) == 0) -} - -func TestMetaListNoRes(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - meta := NewMeta(cmo, "") - - o, err := meta.List("dp", "fred", metav1.ListOptions{}) - assert.ErrorContains(t, err, "No informer found") - assert.Assert(t, len(o) == 0) -} - -func TestMetaGet(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - meta := NewMeta(cmo, "") - - o, err := meta.Get(PodIndex, "fred", metav1.GetOptions{}) - assert.ErrorContains(t, err, "Pod fred not found") - assert.Assert(t, o == nil) -} - -func TestMetaGetNoRes(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - meta := NewMeta(cmo, "") - - o, err := meta.Get("rs", "fred", metav1.GetOptions{}) - assert.ErrorContains(t, err, "No informer found") - assert.Assert(t, o == nil) -} - -func TestMetaRun(t *testing.T) { - f := new(genericclioptions.ConfigFlags) - cmo := NewMockConnection() - m.When(cmo.Config()).ThenReturn(k8s.NewConfig(f)) - meta := NewMeta(cmo, "") - - c := make(chan struct{}) - meta.Run(c) - close(c) -} diff --git a/internal/watch/no_mx.go b/internal/watch/no_mx.go index 8f4bdf41..6c9539a6 100644 --- a/internal/watch/no_mx.go +++ b/internal/watch/no_mx.go @@ -56,14 +56,15 @@ func (p *NodeMetrics) Get(MetaFQN string, opts metav1.GetOptions) (interface{}, // NewNodeMetricsInformer return an informer to return node metrix. func newNodeMetricsInformer(client k8s.Connection, sync time.Duration, idxs cache.Indexers) cache.SharedIndexInformer { pw := newNodeMxWatcher(client) + c, err := client.MXDial() + if err != nil { + log.Error().Err(err).Msg("NodeMetrix dial") + return nil + } return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { - c, err := client.MXDial() - if err != nil { - return nil, err - } l, err := c.MetricsV1beta1().NodeMetricses().List(opts) if err == nil { pw.update(l, false) @@ -71,7 +72,7 @@ func newNodeMetricsInformer(client k8s.Connection, sync time.Duration, idxs cach return l, err }, WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { - pw.Run() + go pw.Run() return pw, nil }, }, @@ -101,30 +102,30 @@ func newNodeMxWatcher(c k8s.Connection) *nodeMxWatcher { // Run watcher to monitor node metrics. func (n *nodeMxWatcher) Run() { - go func() { - defer log.Debug().Msg("Node metrics watcher canceled!") - for { - select { - case <-n.doneChan: - return - case <-time.After(nodeMXRefresh): - c, err := n.client.MXDial() - if err != nil { - return - } - list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{}) - if err != nil { - log.Error().Err(err).Msg("Fetch node metrics") - } - n.update(list, true) + defer log.Debug().Msg("NodeMetrics informer canceled!") + c, err := n.client.MXDial() + if err != nil { + log.Error().Err(err).Msg("NodeMetrix Dial Failed!") + return + } + + for { + select { + case <-n.doneChan: + return + case <-time.After(nodeMXRefresh): + list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{}) + if err != nil { + log.Error().Err(err).Msg("NodeMetrics List Failed!") } + n.update(list, true) } - }() + } } // Stop the metrics informer. func (n *nodeMxWatcher) Stop() { - log.Debug().Msg("Stopping node watcher!") + log.Debug().Msg("Stopping NodeMetrix informer!") close(n.doneChan) close(n.eventChan) } diff --git a/internal/watch/no_mx_test.go b/internal/watch/no_mx_test.go index a14851da..88b9deac 100644 --- a/internal/watch/no_mx_test.go +++ b/internal/watch/no_mx_test.go @@ -1,10 +1,16 @@ package watch import ( + "sync" "testing" "gotest.tools/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" + v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) func TestNodeMXList(t *testing.T) { @@ -24,10 +30,87 @@ func TestNodeMXGet(t *testing.T) { assert.Assert(t, o == nil) } +func TestNodeMXUpdate(t *testing.T) { + cmo := NewMockConnection() + no := newNodeMxWatcher(cmo) + no.cache = map[string]runtime.Object{ + "n1": makeNodeMX("n1", "11m", "11Mi"), + } + + mxx := &mv1beta1.NodeMetricsList{ + Items: []mv1beta1.NodeMetrics{ + *makeNodeMX("n1", "10m", "10Mi"), + }, + } + no.update(mxx, false) + + assert.Equal(t, toQty("10m"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Cpu()) + assert.Equal(t, toQty("10Mi"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Memory()) +} + +func TestNodeMXUpdateNoChange(t *testing.T) { + cmo := NewMockConnection() + no := newNodeMxWatcher(cmo) + no.cache = map[string]runtime.Object{ + "n1": makeNodeMX("n1", "10m", "10Mi"), + } + + mxx := &mv1beta1.NodeMetricsList{ + Items: []mv1beta1.NodeMetrics{ + *makeNodeMX("n1", "10m", "10Mi"), + }, + } + no.update(mxx, false) + + assert.Equal(t, toQty("10m"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Cpu()) + assert.Equal(t, toQty("10Mi"), *no.cache["n1"].(*mv1beta1.NodeMetrics).Usage.Memory()) +} + +func TestNodeMXDelete(t *testing.T) { + cmo := NewMockConnection() + no := newNodeMxWatcher(cmo) + no.cache = map[string]runtime.Object{ + "n1": makeNodeMX("n1", "11m", "11Mi"), + } + + mxx := &mv1beta1.NodeMetricsList{} + no.update(mxx, false) + + assert.Equal(t, 0, len(no.cache)) +} + func TestNodeMXRun(t *testing.T) { cmo := NewMockConnection() w := newNodeMxWatcher(cmo) - w.Run() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + w.Run() + }() + w.Stop() + wg.Wait() +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toQty(s string) resource.Quantity { + q, _ := resource.ParseQuantity(s) + + return q +} + +func makeNodeMX(n, cpu, mem string) *v1beta1.NodeMetrics { + return &v1beta1.NodeMetrics{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + }, + Usage: v1.ResourceList{ + v1.ResourceCPU: toQty(cpu), + v1.ResourceMemory: toQty(mem), + }, + } } diff --git a/internal/watch/no_test.go b/internal/watch/no_test.go index c8cf25d5..e8ccffff 100644 --- a/internal/watch/no_test.go +++ b/internal/watch/no_test.go @@ -10,16 +10,16 @@ import ( func TestNodeList(t *testing.T) { cmo := NewMockConnection() no := NewNode(cmo) - o := no.List("", metav1.ListOptions{}) + assert.Assert(t, o == nil) } func TestNodeGet(t *testing.T) { cmo := NewMockConnection() no := NewNode(cmo) - o, err := no.Get("", metav1.GetOptions{}) + assert.ErrorContains(t, err, "not found") assert.Assert(t, o == nil) } diff --git a/internal/watch/pod.go b/internal/watch/pod.go index 9a507ea3..8d6911d7 100644 --- a/internal/watch/pod.go +++ b/internal/watch/pod.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/derailed/k9s/internal/k8s" - "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" wv1 "k8s.io/client-go/informers/core/v1" @@ -33,22 +32,6 @@ func NewPod(c Connection, ns string) *Pod { } } -func toSelector(s string) map[string]string { - var m map[string]string - ls, err := metav1.ParseToLabelSelector(s) - if err != nil { - log.Error().Err(err).Msg("StringToSel") - return m - } - mSel, err := metav1.LabelSelectorAsMap(ls) - if err != nil { - log.Error().Err(err).Msg("SelToMap") - return m - } - - return mSel -} - // List all pods from store in the given namespace. func (p *Pod) List(ns string, opts metav1.ListOptions) k8s.Collection { var res k8s.Collection @@ -85,24 +68,3 @@ func (p *Pod) Get(fqn string, opts metav1.GetOptions) (interface{}, error) { return o, nil } - -func matchesLabels(labels, selector map[string]string) bool { - if len(selector) == 0 { - return true - } - for k, v := range selector { - la, ok := labels[k] - if !ok || la != v { - return false - } - } - - return true -} - -func matchesNode(name string, selector map[string]string) bool { - if len(selector) == 0 { - return true - } - return selector["spec.nodeName"] == name -} diff --git a/internal/watch/pod_mx.go b/internal/watch/pod_mx.go index fdc97060..2d9d7b51 100644 --- a/internal/watch/pod_mx.go +++ b/internal/watch/pod_mx.go @@ -66,14 +66,15 @@ func (p *PodMetrics) Get(fqn string, opts metav1.GetOptions) (interface{}, error // NewPodMetricsInformer return an informer to return pod metrix. func newPodMetricsInformer(client k8s.Connection, ns string, sync time.Duration, idxs cache.Indexers) cache.SharedIndexInformer { pw := newPodMxWatcher(client, ns) + c, err := client.MXDial() + if err != nil { + log.Error().Err(err).Msg("PodMetrix dial") + return nil + } return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { - c, err := client.MXDial() - if err != nil { - return nil, err - } l, err := c.MetricsV1beta1().PodMetricses(ns).List(opts) if err == nil { pw.update(l, false) @@ -81,7 +82,7 @@ func newPodMetricsInformer(client k8s.Connection, ns string, sync time.Duration, return l, err }, WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { - pw.Run() + go pw.Run() return pw, nil }, }, @@ -113,26 +114,25 @@ func newPodMxWatcher(c k8s.Connection, ns string) *podMxWatcher { // Run watcher to monitor pod metrics. func (p *podMxWatcher) Run() { - go func() { - defer log.Debug().Msg("Podmetrics watcher canceled!") - for { - select { - case <-p.doneChan: - return - case <-time.After(podMXRefresh): - c, err := p.client.MXDial() - if err != nil || !p.client.HasMetrics() { - return - } + defer log.Debug().Msg("PodMetrics informer stopped!") + c, err := p.client.MXDial() + if err != nil { + log.Error().Err(err).Msg("PodMetrix Dial Failed!") + return + } - list, err := c.MetricsV1beta1().PodMetricses(p.ns).List(metav1.ListOptions{}) - if err != nil { - log.Error().Err(err).Msg("Fetch pod metrics") - } - p.update(list, true) + for { + select { + case <-p.doneChan: + return + case <-time.After(podMXRefresh): + list, err := c.MetricsV1beta1().PodMetricses(p.ns).List(metav1.ListOptions{}) + if err != nil { + log.Error().Err(err).Msg("PodMetrics List Failed!") } + p.update(list, true) } - }() + } } func (p *podMxWatcher) update(list *mv1beta1.PodMetricsList, notify bool) { @@ -162,7 +162,6 @@ func (p *podMxWatcher) update(list *mv1beta1.PodMetricsList, notify bool) { } kind = watch.Modified } - if notify { p.eventChan <- watch.Event{Type: kind, Object: v} } @@ -172,7 +171,7 @@ func (p *podMxWatcher) update(list *mv1beta1.PodMetricsList, notify bool) { // Stop the metrics informer. func (p *podMxWatcher) Stop() { - log.Debug().Msg("Stopping pod watcher!") + log.Debug().Msg("Stopping PodMetrix informer!!") close(p.doneChan) close(p.eventChan) } diff --git a/internal/watch/pod_mx_test.go b/internal/watch/pod_mx_test.go index a5dd3074..bd88e9ee 100644 --- a/internal/watch/pod_mx_test.go +++ b/internal/watch/pod_mx_test.go @@ -1,12 +1,15 @@ package watch import ( + "sync" "testing" "github.com/rs/zerolog" "gotest.tools/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" + v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) func init() { @@ -15,17 +18,17 @@ func init() { func TestPodMXList(t *testing.T) { cmo := NewMockConnection() - no := NewPodMetrics(cmo, "") + po := NewPodMetrics(cmo, "") - o := no.List("", metav1.ListOptions{}) + o := po.List("", metav1.ListOptions{}) assert.Assert(t, len(o) == 0) } func TestPodMXGet(t *testing.T) { cmo := NewMockConnection() - no := NewPodMetrics(cmo, "") + po := NewPodMetrics(cmo, "") - o, err := no.Get("", metav1.GetOptions{}) + o, err := po.Get("", metav1.GetOptions{}) assert.ErrorContains(t, err, "No pod metrics") assert.Assert(t, o == nil) } @@ -53,6 +56,80 @@ func TestPodMXRun(t *testing.T) { cmo := NewMockConnection() w := newPodMxWatcher(cmo, "") - w.Run() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + w.Run() + }() + w.Stop() + wg.Wait() +} + +func TestPodMXUpdate(t *testing.T) { + cmo := NewMockConnection() + po := newPodMxWatcher(cmo, "default") + po.cache = map[string]runtime.Object{ + "default/p1": makePodMX("p1", "11m", "11Mi"), + } + + mxx := &mv1beta1.PodMetricsList{ + Items: []mv1beta1.PodMetrics{ + *makePodMX("p1", "10m", "10Mi"), + }, + } + po.update(mxx, false) + + pmx := po.cache["default/p1"].(*mv1beta1.PodMetrics) + assert.Equal(t, toQty("10m"), *pmx.Containers[0].Usage.Cpu()) + assert.Equal(t, toQty("10Mi"), *pmx.Containers[0].Usage.Memory()) +} + +func TestPodMXUpdateNoChange(t *testing.T) { + cmo := NewMockConnection() + po := newPodMxWatcher(cmo, "default") + po.cache = map[string]runtime.Object{ + "default/p1": makePodMX("p1", "10m", "10Mi"), + } + + mxx := &mv1beta1.PodMetricsList{ + Items: []mv1beta1.PodMetrics{ + *makePodMX("p1", "10m", "10Mi"), + }, + } + po.update(mxx, false) + + pmx := po.cache["default/p1"].(*mv1beta1.PodMetrics) + assert.Equal(t, toQty("10m"), *pmx.Containers[0].Usage.Cpu()) + assert.Equal(t, toQty("10Mi"), *pmx.Containers[0].Usage.Memory()) +} + +func TestPodMXDelete(t *testing.T) { + cmo := NewMockConnection() + po := newPodMxWatcher(cmo, "default") + po.cache = map[string]runtime.Object{ + "default/p1": makePodMX("p1", "11m", "11Mi"), + } + + mxx := &mv1beta1.PodMetricsList{} + po.update(mxx, false) + + assert.Equal(t, 0, len(po.cache)) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func makePodMX(name, cpu, mem string) *v1beta1.PodMetrics { + return &v1beta1.PodMetrics{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Containers: []v1beta1.ContainerMetrics{ + {Name: "i1", Usage: makeRes(cpu, mem)}, + {Name: "c1", Usage: makeRes(cpu, mem)}, + }, + } } diff --git a/internal/watch/pod_test.go b/internal/watch/pod_test.go index 14aec340..5c878f7c 100644 --- a/internal/watch/pod_test.go +++ b/internal/watch/pod_test.go @@ -18,8 +18,8 @@ func TestPodList(t *testing.T) { func TestPodGet(t *testing.T) { cmo := NewMockConnection() no := NewPod(cmo, "") - o, err := no.Get("", metav1.GetOptions{}) + assert.ErrorContains(t, err, "not found") assert.Assert(t, o == nil) } diff --git a/main.go b/main.go index 47240c58..7708622b 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,9 @@ package main import ( "os" + "net/http" + _ "net/http/pprof" + "github.com/derailed/k9s/cmd" "github.com/derailed/k9s/internal/config" "github.com/rs/zerolog" @@ -22,5 +25,7 @@ func init() { } func main() { + go http.ListenAndServe(":9000", nil) + cmd.Execute() }