some bugs and perf fixes
parent
8b6898dea7
commit
a5245a98de
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
5
go.mod
5
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
|
||||
|
|
|
|||
4
go.sum
4
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<container> %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)
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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("<Confirm>")
|
||||
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("<<FS %s WATCHER DONE>>", dir)
|
||||
w.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w.Add(dir)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue