some bugs and perf fixes

mine
derailed 2019-05-29 17:32:18 -06:00
parent 8b6898dea7
commit a5245a98de
68 changed files with 1493 additions and 549 deletions

View File

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

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

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

123
internal/config/bench.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,9 @@ package main
import (
"os"
"net/http"
_ "net/http/pprof"
"github.com/derailed/k9s/cmd"
"github.com/derailed/k9s/internal/config"
"github.com/rs/zerolog"
@ -22,5 +25,7 @@ func init() {
}
func main() {
go http.ListenAndServe(":9000", nil)
cmd.Execute()
}