fix for #452
parent
8b6b9a79b0
commit
aab46f6aad
|
|
@ -1,6 +1,6 @@
|
||||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||||
|
|
||||||
# Release v0.10.4
|
# Release v0.10.5
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||||
|
|
||||||
|
# Release v0.10.6
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
|
||||||
|
|
||||||
|
Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change Logs
|
||||||
|
|
||||||
|
Maintenance release!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved Bugs/Features
|
||||||
|
|
||||||
|
* [Issue #452](https://github.com/derailed/k9s/issues/452)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
@ -41,6 +41,7 @@ func NewGVR(gvr string) GVR {
|
||||||
return GVR{raw: gvr, g: g, v: v, r: r, sr: sr}
|
return GVR{raw: gvr, g: g, v: v, r: r, sr: sr}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewGVRFromMeta builds a gvr from resource metadata.
|
||||||
func NewGVRFromMeta(a metav1.APIResource) GVR {
|
func NewGVRFromMeta(a metav1.APIResource) GVR {
|
||||||
return GVR{
|
return GVR{
|
||||||
raw: path.Join(a.Group, a.Version, a.Name),
|
raw: path.Join(a.Group, a.Version, a.Name),
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ type Benchmark struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Benchmark{}
|
var _ Accessor = (*Benchmark)(nil)
|
||||||
var _ Nuker = &Benchmark{}
|
var _ Nuker = (*Benchmark)(nil)
|
||||||
|
|
||||||
// Delete a Benchmark.
|
// Delete a Benchmark.
|
||||||
func (d *Benchmark) Delete(path string, cascade, force bool) error {
|
func (d *Benchmark) Delete(path string, cascade, force bool) error {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ type Container struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Container{}
|
var _ Accessor = (*Container)(nil)
|
||||||
var _ Loggable = &Container{}
|
var _ Loggable = (*Container)(nil)
|
||||||
|
|
||||||
// TailLogs tails a given container logs
|
// TailLogs tails a given container logs
|
||||||
func (c *Container) TailLogs(ctx context.Context, logChan chan<- string, opts LogOptions) error {
|
func (c *Container) TailLogs(ctx context.Context, logChan chan<- string, opts LogOptions) error {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ type Context struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Context{}
|
var _ Accessor = (*Context)(nil)
|
||||||
var _ Switchable = &Context{}
|
var _ Switchable = (*Context)(nil)
|
||||||
|
|
||||||
func (c *Context) config() *client.Config {
|
func (c *Context) config() *client.Config {
|
||||||
return c.Factory.Client().Config()
|
return c.Factory.Client().Config()
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ type CronJob struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &CronJob{}
|
var _ Accessor = (*CronJob)(nil)
|
||||||
var _ Runnable = &CronJob{}
|
var _ Runnable = (*CronJob)(nil)
|
||||||
|
|
||||||
// Run a CronJob.
|
// Run a CronJob.
|
||||||
func (c *CronJob) Run(path string) error {
|
func (c *CronJob) Run(path string) error {
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ type Deployment struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Deployment{}
|
var _ Accessor = (*Deployment)(nil)
|
||||||
var _ Loggable = &Deployment{}
|
var _ Loggable = (*Deployment)(nil)
|
||||||
var _ Restartable = &Deployment{}
|
var _ Restartable = (*Deployment)(nil)
|
||||||
var _ Scalable = &Deployment{}
|
var _ Scalable = (*Deployment)(nil)
|
||||||
|
|
||||||
// Scale a Deployment.
|
// Scale a Deployment.
|
||||||
func (d *Deployment) Scale(path string, replicas int32) error {
|
func (d *Deployment) Scale(path string, replicas int32) error {
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ type DaemonSet struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &DaemonSet{}
|
var _ Accessor = (*DaemonSet)(nil)
|
||||||
var _ Loggable = &DaemonSet{}
|
var _ Loggable = (*DaemonSet)(nil)
|
||||||
var _ Restartable = &DaemonSet{}
|
var _ Restartable = (*DaemonSet)(nil)
|
||||||
|
|
||||||
// Restart a DaemonSet rollout.
|
// Restart a DaemonSet rollout.
|
||||||
func (d *DaemonSet) Restart(path string) error {
|
func (d *DaemonSet) Restart(path string) error {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ type Job struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Job{}
|
var _ Accessor = (*Job)(nil)
|
||||||
var _ Loggable = &Job{}
|
var _ Loggable = (*Job)(nil)
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this Job.
|
// TailLogs tail logs for all pods represented by this Job.
|
||||||
func (j *Job) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
func (j *Job) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ type Pod struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Pod{}
|
var _ Accessor = (*Pod)(nil)
|
||||||
var _ Loggable = &Pod{}
|
var _ Loggable = (*Pod)(nil)
|
||||||
|
|
||||||
// Logs fetch container logs for a given pod and container.
|
// Logs fetch container logs for a given pod and container.
|
||||||
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ type PortForward struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &PortForward{}
|
var _ Accessor = (*PortForward)(nil)
|
||||||
var _ Nuker = &PortForward{}
|
var _ Nuker = (*PortForward)(nil)
|
||||||
|
|
||||||
// Delete a portforward.
|
// Delete a portforward.
|
||||||
func (p *PortForward) Delete(path string, cascade, force bool) error {
|
func (p *PortForward) Delete(path string, cascade, force bool) error {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ type ScreenDump struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &ScreenDump{}
|
var _ Accessor = (*ScreenDump)(nil)
|
||||||
var _ Nuker = &ScreenDump{}
|
var _ Nuker = (*ScreenDump)(nil)
|
||||||
|
|
||||||
// Delete a ScreenDump.
|
// Delete a ScreenDump.
|
||||||
func (d *ScreenDump) Delete(path string, cascade, force bool) error {
|
func (d *ScreenDump) Delete(path string, cascade, force bool) error {
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ type StatefulSet struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &StatefulSet{}
|
var _ Accessor = (*StatefulSet)(nil)
|
||||||
var _ Loggable = &StatefulSet{}
|
var _ Loggable = (*StatefulSet)(nil)
|
||||||
var _ Restartable = &StatefulSet{}
|
var _ Restartable = (*StatefulSet)(nil)
|
||||||
var _ Scalable = &StatefulSet{}
|
var _ Scalable = (*StatefulSet)(nil)
|
||||||
|
|
||||||
// Scale a StatefulSet.
|
// Scale a StatefulSet.
|
||||||
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
func (s *StatefulSet) Scale(path string, replicas int32) error {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ type Service struct {
|
||||||
Generic
|
Generic
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Accessor = &Service{}
|
var _ Accessor = (*Service)(nil)
|
||||||
var _ Loggable = &Service{}
|
var _ Loggable = (*Service)(nil)
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this Service.
|
// TailLogs tail logs for all pods represented by this Service.
|
||||||
func (s *Service) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
func (s *Service) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error {
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,5 @@ const (
|
||||||
KeyCluster ContextKey = "cluster"
|
KeyCluster ContextKey = "cluster"
|
||||||
KeyApp ContextKey = "app"
|
KeyApp ContextKey = "app"
|
||||||
KeyStyles ContextKey = "styles"
|
KeyStyles ContextKey = "styles"
|
||||||
|
KeyMetrics ContextKey = "metrics"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,18 @@ package model
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ render.NodeWithMetrics = &NodeWithMetrics{}
|
type NodeMetricsFunc func() (*mv1beta1.NodeMetricsList, error)
|
||||||
|
|
||||||
// Node represents a node model.
|
// Node represents a node model.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
|
|
@ -23,65 +22,50 @@ type Node struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a collection of node resources.
|
// List returns a collection of node resources.
|
||||||
func (n *Node) List(_ context.Context) ([]runtime.Object, error) {
|
func (n *Node) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
defer func(t time.Time) {
|
||||||
|
log.Debug().Msgf("LIST NODES elapsed %v", time.Since(t))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
|
nmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.NodeMetricsList)
|
||||||
|
if !ok {
|
||||||
|
log.Warn().Msgf("No node metrics available in context")
|
||||||
|
}
|
||||||
|
|
||||||
nn, err := dao.FetchNodes(n.factory)
|
nn, err := dao.FetchNodes(n.factory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
oo := make([]runtime.Object, len(nn.Items))
|
oo := make([]runtime.Object, len(nn.Items))
|
||||||
for i := range nn.Items {
|
for i, no := range nn.Items {
|
||||||
o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nn.Items[i])
|
o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nn.Items[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
oo[i] = &unstructured.Unstructured{Object: o}
|
oo[i] = &render.NodeWithMetrics{
|
||||||
|
Raw: &unstructured.Unstructured{Object: o},
|
||||||
|
MX: nodeMetricsFor(MetaFQN(no.ObjectMeta), nmx),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return oo, nil
|
return oo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nameFromMeta(m map[string]interface{}) string {
|
|
||||||
meta, ok := m["metadata"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return "n/a"
|
|
||||||
}
|
|
||||||
|
|
||||||
name, ok := meta["name"].(string)
|
|
||||||
if !ok {
|
|
||||||
return "n/a"
|
|
||||||
}
|
|
||||||
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hydrate returns nodes as rows.
|
// Hydrate returns nodes as rows.
|
||||||
func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
mx := client.NewMetricsServer(n.factory.Client())
|
defer func(t time.Time) {
|
||||||
mmx, err := mx.FetchNodesMetrics()
|
log.Debug().Msgf("HYDRATE NODES elapsed %v", time.Since(t))
|
||||||
if err != nil {
|
}(time.Now())
|
||||||
log.Warn().Err(err).Msg("No node metrics")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, o := range oo {
|
for i, o := range oo {
|
||||||
no, ok := o.(*unstructured.Unstructured)
|
nmx, ok := o.(*render.NodeWithMetrics)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("expecting unstructured but got %T", o)
|
return fmt.Errorf("expecting *NodeWithMetrics but got %T", o)
|
||||||
}
|
|
||||||
pods, err := n.nodePods(n.factory, nameFromMeta(no.Object))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var row render.Row
|
||||||
row render.Row
|
if err := re.Render(nmx, render.ClusterScope, &row); err != nil {
|
||||||
nmx = NodeWithMetrics{
|
|
||||||
object: no,
|
|
||||||
mx: nodeMetricsFor(o, mmx),
|
|
||||||
pods: pods,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if err := re.Render(&nmx, "", &row); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rr[i] = row
|
rr[i] = row
|
||||||
|
|
@ -90,8 +74,10 @@ func (n *Node) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics {
|
// ----------------------------------------------------------------------------
|
||||||
fqn := extractFQN(o)
|
// Helpers...
|
||||||
|
|
||||||
|
func nodeMetricsFor(fqn string, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics {
|
||||||
for _, mx := range mmx.Items {
|
for _, mx := range mmx.Items {
|
||||||
if MetaFQN(mx.ObjectMeta) == fqn {
|
if MetaFQN(mx.ObjectMeta) == fqn {
|
||||||
return &mx
|
return &mx
|
||||||
|
|
@ -99,55 +85,3 @@ func nodeMetricsFor(o runtime.Object, mmx *mv1beta1.NodeMetricsList) *mv1beta1.N
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) nodePods(f dao.Factory, node string) ([]*v1.Pod, error) {
|
|
||||||
pp, err := f.List("v1/pods", render.AllNamespaces, true, labels.Everything())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pods := make([]*v1.Pod, 0, len(pp))
|
|
||||||
for _, p := range pp {
|
|
||||||
o, ok := p.(*unstructured.Unstructured)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("expecting unstructured but got %T", p)
|
|
||||||
}
|
|
||||||
var pod v1.Pod
|
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &pod)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Converting Pod")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if pod.Spec.NodeName != node || pod.Status.Phase != v1.PodSucceeded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pods = append(pods, &pod)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pods, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Helpers...
|
|
||||||
|
|
||||||
// NodeWithMetrics represents a node with its associated metrics.
|
|
||||||
type NodeWithMetrics struct {
|
|
||||||
object runtime.Object
|
|
||||||
mx *mv1beta1.NodeMetrics
|
|
||||||
pods []*v1.Pod
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object returns a node.
|
|
||||||
func (n *NodeWithMetrics) Object() runtime.Object {
|
|
||||||
return n.object
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics returns the node metrics.
|
|
||||||
func (n *NodeWithMetrics) Metrics() *mv1beta1.NodeMetrics {
|
|
||||||
return n.mx
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pods return pods running on this node.
|
|
||||||
func (n *NodeWithMetrics) Pods() []*v1.Pod {
|
|
||||||
return n.pods
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNodeHydrate(t *testing.T) {
|
||||||
|
f := makeFactory()
|
||||||
|
var no model.Node
|
||||||
|
no.Init("", "v1/nodes", f)
|
||||||
|
|
||||||
|
o := render.NodeWithMetrics{Raw: load(t, "n1")}
|
||||||
|
rr := make(render.Rows, 1)
|
||||||
|
assert.Nil(t, no.Hydrate([]runtime.Object{&o}, rr, render.Node{}))
|
||||||
|
assert.Equal(t, 1, len(rr))
|
||||||
|
assert.Equal(t, "minikube", rr[0].ID)
|
||||||
|
assert.Equal(t, render.Fields{
|
||||||
|
"minikube",
|
||||||
|
"Ready",
|
||||||
|
"master",
|
||||||
|
"v1.17.0",
|
||||||
|
"4.19.81",
|
||||||
|
"192.168.64.6",
|
||||||
|
"<none>",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
}, rr[0].Fields[:len(rr[0].Fields)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNodeHydrate(b *testing.B) {
|
||||||
|
f := makeFactory()
|
||||||
|
var no model.Node
|
||||||
|
no.Init("", "v1/nodes", f)
|
||||||
|
o := load(b, "n1")
|
||||||
|
rr := make(render.Rows, 1)
|
||||||
|
|
||||||
|
oo := []runtime.Object{&render.NodeWithMetrics{Raw: o}}
|
||||||
|
re := render.Node{}
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
no.Hydrate(oo, rr, re)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func load(t assert.TestingT, n string) *unstructured.Unstructured {
|
||||||
|
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
var o unstructured.Unstructured
|
||||||
|
err = json.Unmarshal(raw, &o)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,9 @@ package model
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
|
@ -21,11 +21,20 @@ type Pod struct {
|
||||||
|
|
||||||
// List returns a collection of nodes.
|
// List returns a collection of nodes.
|
||||||
func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
|
func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
defer func(t time.Time) {
|
||||||
|
log.Debug().Msgf("LIST PODS elapsed %v", time.Since(t))
|
||||||
|
}(time.Now())
|
||||||
|
|
||||||
oo, err := p.Resource.List(ctx)
|
oo, err := p.Resource.List(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return oo, err
|
return oo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
|
||||||
|
if !ok {
|
||||||
|
log.Warn().Msgf("expecting context PodMetricsList")
|
||||||
|
}
|
||||||
|
|
||||||
sel, ok := ctx.Value(internal.KeyFields).(string)
|
sel, ok := ctx.Value(internal.KeyFields).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return oo, nil
|
return oo, nil
|
||||||
|
|
@ -40,14 +49,19 @@ func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
u, ok := o.(*unstructured.Unstructured)
|
u, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("expecting unstructured but got `%T", o)
|
return res, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
|
||||||
}
|
}
|
||||||
|
if nodeName == "" {
|
||||||
|
res = append(res, &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
spec, ok := u.Object["spec"].(map[string]interface{})
|
spec, ok := u.Object["spec"].(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, fmt.Errorf("expecting interface map but got `%T", o)
|
return res, fmt.Errorf("expecting interface map but got `%T", o)
|
||||||
}
|
}
|
||||||
if nodeName == "" || spec["nodeName"] == nodeName {
|
if spec["nodeName"] == nodeName {
|
||||||
res = append(res, o)
|
res = append(res, &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,19 +70,18 @@ func (p *Pod) List(ctx context.Context) ([]runtime.Object, error) {
|
||||||
|
|
||||||
// Hydrate returns pod resources as rows.
|
// Hydrate returns pod resources as rows.
|
||||||
func (p *Pod) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
func (p *Pod) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
mx := client.NewMetricsServer(p.factory.Client())
|
defer func(t time.Time) {
|
||||||
mmx, err := mx.FetchPodsMetrics(p.namespace)
|
log.Debug().Msgf("HYDRATE PODS elapsed %v", time.Since(t))
|
||||||
if err != nil {
|
}(time.Now())
|
||||||
log.Warn().Err(err).Msgf("No metrics found for pod")
|
|
||||||
}
|
|
||||||
|
|
||||||
var index int
|
var index int
|
||||||
for _, o := range oo {
|
for _, o := range oo {
|
||||||
var (
|
po, ok := o.(*render.PodWithMetrics)
|
||||||
row render.Row
|
if !ok {
|
||||||
pmx = PodWithMetrics{object: o, mx: podMetricsFor(o, mmx)}
|
return fmt.Errorf("expecting *PodWithMetric but got %T", po)
|
||||||
)
|
}
|
||||||
if err := re.Render(&pmx, p.namespace, &row); err != nil {
|
var row render.Row
|
||||||
|
if err := re.Render(po, p.namespace, &row); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rr[index] = row
|
rr[index] = row
|
||||||
|
|
@ -78,6 +91,9 @@ func (p *Pod) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics {
|
func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics {
|
||||||
fqn := extractFQN(o)
|
fqn := extractFQN(o)
|
||||||
for _, mx := range mmx.Items {
|
for _, mx := range mmx.Items {
|
||||||
|
|
@ -87,19 +103,3 @@ func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.Pod
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodWithMetrics represents a pod and its metrics.
|
|
||||||
type PodWithMetrics struct {
|
|
||||||
object runtime.Object
|
|
||||||
mx *mv1beta1.PodMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object returns a pod.
|
|
||||||
func (p *PodWithMetrics) Object() runtime.Object {
|
|
||||||
return p.object
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics returns the metrics associated with the pod.
|
|
||||||
func (p *PodWithMetrics) Metrics() *mv1beta1.PodMetrics {
|
|
||||||
return p.mx
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPodHydrate(t *testing.T) {
|
||||||
|
f := makeFactory()
|
||||||
|
var po model.Pod
|
||||||
|
po.Init("", "v1/pods", f)
|
||||||
|
|
||||||
|
o := render.PodWithMetrics{Raw: load(t, "p1")}
|
||||||
|
rr := make(render.Rows, 1)
|
||||||
|
assert.Nil(t, po.Hydrate([]runtime.Object{&o}, rr, render.Pod{}))
|
||||||
|
assert.Equal(t, 1, len(rr))
|
||||||
|
assert.Equal(t, "default/nginx-7fb78fb6d8-2w75j", rr[0].ID)
|
||||||
|
assert.Equal(t, render.Fields{
|
||||||
|
"default",
|
||||||
|
"nginx-7fb78fb6d8-2w75j",
|
||||||
|
"1/1",
|
||||||
|
"Running",
|
||||||
|
"0",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"n/a",
|
||||||
|
"10.44.0.229",
|
||||||
|
"gke-k9s-default-pool-0fa2fb89-lbtf",
|
||||||
|
"GA",
|
||||||
|
}, rr[0].Fields[:len(rr[0].Fields)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPodHydrate(b *testing.B) {
|
||||||
|
f := makeFactory()
|
||||||
|
var po model.Pod
|
||||||
|
po.Init("", "v1/pods", f)
|
||||||
|
o := load(b, "p1")
|
||||||
|
rr := make(render.Rows, 1)
|
||||||
|
|
||||||
|
oo := []runtime.Object{&render.PodWithMetrics{Raw: o}}
|
||||||
|
re := render.Pod{}
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
po.Hydrate(oo, rr, re)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Node",
|
||||||
|
"metadata": {
|
||||||
|
"annotations": {
|
||||||
|
"kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock",
|
||||||
|
"node.alpha.kubernetes.io/ttl": "0",
|
||||||
|
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
|
||||||
|
},
|
||||||
|
"creationTimestamp": "2019-12-31T20:49:21Z",
|
||||||
|
"labels": {
|
||||||
|
"beta.kubernetes.io/arch": "amd64",
|
||||||
|
"beta.kubernetes.io/os": "linux",
|
||||||
|
"kubernetes.io/arch": "amd64",
|
||||||
|
"kubernetes.io/hostname": "minikube",
|
||||||
|
"kubernetes.io/os": "linux",
|
||||||
|
"node-role.kubernetes.io/master": ""
|
||||||
|
},
|
||||||
|
"name": "minikube",
|
||||||
|
"resourceVersion": "214450",
|
||||||
|
"selfLink": "/api/v1/nodes/minikube",
|
||||||
|
"uid": "a33a26f0-7688-47b6-8dbf-5a04ea7f43d4"
|
||||||
|
},
|
||||||
|
"spec": {},
|
||||||
|
"status": {
|
||||||
|
"addresses": [
|
||||||
|
{
|
||||||
|
"address": "192.168.64.6",
|
||||||
|
"type": "InternalIP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "minikube",
|
||||||
|
"type": "Hostname"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"allocatable": {
|
||||||
|
"cpu": "4",
|
||||||
|
"ephemeral-storage": "16954240Ki",
|
||||||
|
"hugepages-2Mi": "0",
|
||||||
|
"memory": "8163684Ki",
|
||||||
|
"pods": "110"
|
||||||
|
},
|
||||||
|
"capacity": {
|
||||||
|
"cpu": "4",
|
||||||
|
"ephemeral-storage": "16954240Ki",
|
||||||
|
"hugepages-2Mi": "0",
|
||||||
|
"memory": "8163684Ki",
|
||||||
|
"pods": "110"
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"lastHeartbeatTime": "2020-01-01T22:05:55Z",
|
||||||
|
"lastTransitionTime": "2019-12-31T20:49:18Z",
|
||||||
|
"message": "kubelet has sufficient memory available",
|
||||||
|
"reason": "KubeletHasSufficientMemory",
|
||||||
|
"status": "False",
|
||||||
|
"type": "MemoryPressure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lastHeartbeatTime": "2020-01-01T22:05:55Z",
|
||||||
|
"lastTransitionTime": "2019-12-31T20:49:18Z",
|
||||||
|
"message": "kubelet has no disk pressure",
|
||||||
|
"reason": "KubeletHasNoDiskPressure",
|
||||||
|
"status": "False",
|
||||||
|
"type": "DiskPressure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lastHeartbeatTime": "2020-01-01T22:05:55Z",
|
||||||
|
"lastTransitionTime": "2019-12-31T20:49:18Z",
|
||||||
|
"message": "kubelet has sufficient PID available",
|
||||||
|
"reason": "KubeletHasSufficientPID",
|
||||||
|
"status": "False",
|
||||||
|
"type": "PIDPressure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lastHeartbeatTime": "2020-01-01T22:05:55Z",
|
||||||
|
"lastTransitionTime": "2019-12-31T20:49:22Z",
|
||||||
|
"message": "kubelet is posting ready status",
|
||||||
|
"reason": "KubeletReady",
|
||||||
|
"status": "True",
|
||||||
|
"type": "Ready"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"daemonEndpoints": {
|
||||||
|
"kubeletEndpoint": {
|
||||||
|
"Port": 10250
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"quay.io/kubernetes-ingress-controller/nginx-ingress-controller@sha256:d0b22f715fcea5598ef7f869d308b55289a3daaa12922fa52a1abf17703c88e7",
|
||||||
|
"quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1"
|
||||||
|
],
|
||||||
|
"sizeBytes": 483167446
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/proxyv2@sha256:236527816ff67f8492d7286775e09c28e207aee2f6f3c3d9258cd2248af4afa5",
|
||||||
|
"istio/proxyv2:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 369614978
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"quay.io/kiali/kiali@sha256:60ceb57682e95fa3fb7c6e12d797f21c9e242c5583fa024a859d1085d0985c7b",
|
||||||
|
"quay.io/kiali/kiali:v0.20"
|
||||||
|
],
|
||||||
|
"sizeBytes": 344083595
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/kubectl@sha256:a94f8f992bc1e996319a58ff934f9c5e6658e2338fb59e1d937f919b8146d050",
|
||||||
|
"istio/kubectl:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 341145787
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/galley@sha256:786bb02b6d425697826ce740d723664beababf7a513eb8d4c95b42b35a99e91d",
|
||||||
|
"istio/galley:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 306543175
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/pilot@sha256:ab08845a7f4d1fd44c8481b35161a8da0cbf880f3d4f690740aec27350758a95",
|
||||||
|
"istio/pilot:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 303914365
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/etcd@sha256:4afb99b4690b418ffc2ceb67e1a17376457e441c1f09ab55447f0aaf992fa646",
|
||||||
|
"k8s.gcr.io/etcd:3.4.3-0"
|
||||||
|
],
|
||||||
|
"sizeBytes": 288426917
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"grafana/grafana@sha256:d66b41cf7e0586274ca3e15e03299e4cfde48019fd756bb97cc9db57da9b0c86",
|
||||||
|
"grafana/grafana:6.1.6"
|
||||||
|
],
|
||||||
|
"sizeBytes": 245005426
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/kube-apiserver@sha256:e3ec33d533257902ad9ebe3d399c17710e62009201a7202aec941e351545d662",
|
||||||
|
"k8s.gcr.io/kube-apiserver:v1.17.0"
|
||||||
|
],
|
||||||
|
"sizeBytes": 170957331
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/kube-controller-manager@sha256:0438efb5098a2ca634ea8c6b0d804742b733d0d13fd53cf62c73e32c659a3c39",
|
||||||
|
"k8s.gcr.io/kube-controller-manager:v1.17.0"
|
||||||
|
],
|
||||||
|
"sizeBytes": 160877075
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/kube-proxy@sha256:b2ba9441af30261465e5c41be63e462d0050b09ad280001ae731f399b2b00b75",
|
||||||
|
"k8s.gcr.io/kube-proxy:v1.17.0"
|
||||||
|
],
|
||||||
|
"sizeBytes": 115960823
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52",
|
||||||
|
"k8s.gcr.io/nginx-slim:0.8"
|
||||||
|
],
|
||||||
|
"sizeBytes": 110487599
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"prom/prometheus@sha256:1224ee30a3be668e0b22444773c4c1b750778af492094b6cd375c780c7526e22",
|
||||||
|
"prom/prometheus:v2.8.0"
|
||||||
|
],
|
||||||
|
"sizeBytes": 108629897
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/mixer@sha256:886726967363477eeba4cbf48675b058bcf833c932763b0964db80390fc06ceb",
|
||||||
|
"istio/mixer:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 97783922
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/kube-scheduler@sha256:5215c4216a65f7e76c1895ba951a12dc1c947904a91810fc66a544ff1d7e87db",
|
||||||
|
"k8s.gcr.io/kube-scheduler:v1.17.0"
|
||||||
|
],
|
||||||
|
"sizeBytes": 94431763
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"kubernetesui/dashboard:v2.0.0-beta8"
|
||||||
|
],
|
||||||
|
"sizeBytes": 90835427
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/kube-addon-manager:v9.0.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 83076028
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"gcr.io/k8s-minikube/storage-provisioner:v1.8.1"
|
||||||
|
],
|
||||||
|
"sizeBytes": 80815640
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/citadel@sha256:1e8065b277cb79a32ef617f7af468f9afe5b21ec2e0b42245d029c59fe3ce435",
|
||||||
|
"istio/citadel:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 68454561
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"istio/sidecar_injector@sha256:c8f6f5fb1bb2434f68199e06b124e85dc58a3879bf1275a4d39c400836bd3ca4",
|
||||||
|
"istio/sidecar_injector:1.2.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 63917960
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/metrics-server-amd64@sha256:49a9f12f7067d11f42c803dbe61ed2c1299959ad85cb315b25ff7eef8e6b8892",
|
||||||
|
"k8s.gcr.io/metrics-server-amd64:v0.2.1"
|
||||||
|
],
|
||||||
|
"sizeBytes": 42541759
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/coredns@sha256:7ec975f167d815311a7136c32e70735f0d00b73781365df1befd46ed35bd4fe7",
|
||||||
|
"k8s.gcr.io/coredns:1.6.5"
|
||||||
|
],
|
||||||
|
"sizeBytes": 41578211
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"kubernetesui/metrics-scraper:v1.0.2"
|
||||||
|
],
|
||||||
|
"sizeBytes": 40101552
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"jaegertracing/all-in-one@sha256:29c921747eddfa96c97cf96aac0180e97bfdfcbea25e230daef09711103d1f61",
|
||||||
|
"jaegertracing/all-in-one:1.9"
|
||||||
|
],
|
||||||
|
"sizeBytes": 37328894
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"k8s.gcr.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea",
|
||||||
|
"k8s.gcr.io/pause:3.1"
|
||||||
|
],
|
||||||
|
"sizeBytes": 742472
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nodeInfo": {
|
||||||
|
"architecture": "amd64",
|
||||||
|
"bootID": "478c895b-009b-4b6e-9115-63502eaa68cb",
|
||||||
|
"containerRuntimeVersion": "docker://19.3.5",
|
||||||
|
"kernelVersion": "4.19.81",
|
||||||
|
"kubeProxyVersion": "v1.17.0",
|
||||||
|
"kubeletVersion": "v1.17.0",
|
||||||
|
"machineID": "6c484e2bfebf46f2ac854c484bcfa392",
|
||||||
|
"operatingSystem": "linux",
|
||||||
|
"osImage": "Buildroot 2019.02.7",
|
||||||
|
"systemUUID": "dbc511ea-0000-0000-a42f-acde48001122"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"metadata": {
|
||||||
|
"annotations": {
|
||||||
|
"kubectl.kubernetes.io/restartedAt": "2019-12-31T12:26:47-07:00"
|
||||||
|
},
|
||||||
|
"creationTimestamp": "2019-12-31T19:27:22Z",
|
||||||
|
"generateName": "nginx-7fb78fb6d8-",
|
||||||
|
"labels": {
|
||||||
|
"app": "nginx",
|
||||||
|
"pod-template-hash": "7fb78fb6d8"
|
||||||
|
},
|
||||||
|
"name": "nginx-7fb78fb6d8-2w75j",
|
||||||
|
"namespace": "default",
|
||||||
|
"ownerReferences": [
|
||||||
|
{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"blockOwnerDeletion": true,
|
||||||
|
"controller": true,
|
||||||
|
"kind": "ReplicaSet",
|
||||||
|
"name": "nginx-7fb78fb6d8",
|
||||||
|
"uid": "7ccd0600-2c03-11ea-883f-42010a800044"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resourceVersion": "87290191",
|
||||||
|
"selfLink": "/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j",
|
||||||
|
"uid": "91bb1cf2-2c03-11ea-883f-42010a800044"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"containers": [
|
||||||
|
{
|
||||||
|
"image": "k8s.gcr.io/nginx-slim:0.8",
|
||||||
|
"imagePullPolicy": "IfNotPresent",
|
||||||
|
"name": "nginx",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"containerPort": 80,
|
||||||
|
"protocol": "TCP"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"limits": {
|
||||||
|
"cpu": "200m",
|
||||||
|
"memory": "20Mi"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"cpu": "200m",
|
||||||
|
"memory": "20Mi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"terminationMessagePath": "/dev/termination-log",
|
||||||
|
"terminationMessagePolicy": "File",
|
||||||
|
"volumeMounts": [
|
||||||
|
{
|
||||||
|
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||||
|
"name": "default-token-dsl46",
|
||||||
|
"readOnly": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dnsPolicy": "ClusterFirst",
|
||||||
|
"enableServiceLinks": true,
|
||||||
|
"nodeName": "gke-k9s-default-pool-0fa2fb89-lbtf",
|
||||||
|
"priority": 0,
|
||||||
|
"restartPolicy": "Always",
|
||||||
|
"schedulerName": "default-scheduler",
|
||||||
|
"securityContext": {},
|
||||||
|
"serviceAccount": "default",
|
||||||
|
"serviceAccountName": "default",
|
||||||
|
"terminationGracePeriodSeconds": 30,
|
||||||
|
"tolerations": [
|
||||||
|
{
|
||||||
|
"effect": "NoExecute",
|
||||||
|
"key": "node.kubernetes.io/not-ready",
|
||||||
|
"operator": "Exists",
|
||||||
|
"tolerationSeconds": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"effect": "NoExecute",
|
||||||
|
"key": "node.kubernetes.io/unreachable",
|
||||||
|
"operator": "Exists",
|
||||||
|
"tolerationSeconds": 300
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
{
|
||||||
|
"name": "default-token-dsl46",
|
||||||
|
"secret": {
|
||||||
|
"defaultMode": 420,
|
||||||
|
"secretName": "default-token-dsl46"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"lastProbeTime": null,
|
||||||
|
"lastTransitionTime": "2019-12-31T19:27:23Z",
|
||||||
|
"status": "True",
|
||||||
|
"type": "Initialized"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lastProbeTime": null,
|
||||||
|
"lastTransitionTime": "2019-12-31T19:27:25Z",
|
||||||
|
"status": "True",
|
||||||
|
"type": "Ready"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lastProbeTime": null,
|
||||||
|
"lastTransitionTime": "2019-12-31T19:27:25Z",
|
||||||
|
"status": "True",
|
||||||
|
"type": "ContainersReady"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lastProbeTime": null,
|
||||||
|
"lastTransitionTime": "2019-12-31T19:27:22Z",
|
||||||
|
"status": "True",
|
||||||
|
"type": "PodScheduled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"containerStatuses": [
|
||||||
|
{
|
||||||
|
"containerID": "docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809",
|
||||||
|
"image": "k8s.gcr.io/nginx-slim:0.8",
|
||||||
|
"imageID": "docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52",
|
||||||
|
"lastState": {},
|
||||||
|
"name": "nginx",
|
||||||
|
"ready": true,
|
||||||
|
"restartCount": 0,
|
||||||
|
"state": {
|
||||||
|
"running": {
|
||||||
|
"startedAt": "2019-12-31T19:27:24Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hostIP": "10.128.0.15",
|
||||||
|
"phase": "Running",
|
||||||
|
"podIP": "10.44.0.229",
|
||||||
|
"qosClass": "Guaranteed",
|
||||||
|
"startTime": "2019-12-31T19:27:23Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,11 @@ package render
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
v1 "k8s.io/api/core/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigMap renders a K8s ConfigMap to screen.
|
// ConfigMap renders a K8s ConfigMap to screen.
|
||||||
|
|
@ -33,27 +33,78 @@ func (ConfigMap) Header(ns string) HeaderRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
|
// BOZO!! 44allocs down to 5allocs avoiding marshal??
|
||||||
func (c ConfigMap) Render(o interface{}, ns string, r *Row) error {
|
func (c ConfigMap) Render(o interface{}, ns string, r *Row) error {
|
||||||
raw, ok := o.(*unstructured.Unstructured)
|
raw, ok := o.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected ConfigMap, but got %T", o)
|
return fmt.Errorf("Expected ConfigMap, but got %T", o)
|
||||||
}
|
}
|
||||||
var cm v1.ConfigMap
|
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm)
|
meta, ok := raw.Object["metadata"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("No meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, nss := extractMetaField(meta, "name"), extractMetaField(meta, "namespace")
|
||||||
|
r.ID = FQN(nss, n)
|
||||||
|
r.Fields = make(Fields, 0, len(c.Header(ns)))
|
||||||
|
if isAllNamespace(ns) {
|
||||||
|
r.Fields = append(r.Fields, nss)
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int
|
||||||
|
data, ok := raw.Object["data"]
|
||||||
|
if ok {
|
||||||
|
d, ok := data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expecting map but got %T", raw.Object["data"])
|
||||||
|
}
|
||||||
|
size = len(d)
|
||||||
|
}
|
||||||
|
t, err := extractMetaTime(meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ID = MetaFQN(cm.ObjectMeta)
|
|
||||||
r.Fields = make(Fields, 0, len(c.Header(ns)))
|
|
||||||
if isAllNamespace(ns) {
|
|
||||||
r.Fields = append(r.Fields, cm.Namespace)
|
|
||||||
}
|
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
cm.Name,
|
n,
|
||||||
strconv.Itoa(len(cm.Data)),
|
strconv.Itoa(size),
|
||||||
toAge(cm.ObjectMeta.CreationTimestamp),
|
toAge(t),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// var cm v1.ConfigMap
|
||||||
|
// err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// r.ID = MetaFQN(cm.ObjectMeta)
|
||||||
|
// r.Fields = make(Fields, 0, len(c.Header(ns)))
|
||||||
|
// if isAllNamespace(ns) {
|
||||||
|
// r.Fields = append(r.Fields, cm.Namespace)
|
||||||
|
// }
|
||||||
|
// r.Fields = append(r.Fields,
|
||||||
|
// cm.Name,
|
||||||
|
// strconv.Itoa(len(cm.Data)),
|
||||||
|
// toAge(cm.ObjectMeta.CreationTimestamp),
|
||||||
|
// )
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractMetaTime(m map[string]interface{}) (metav1.Time, error) {
|
||||||
|
f, ok := m["creationTimestamp"]
|
||||||
|
if !ok {
|
||||||
|
return metav1.Time{}, fmt.Errorf("failed to extract time from meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
t, ok := f.(string)
|
||||||
|
if !ok {
|
||||||
|
return metav1.Time{}, fmt.Errorf("failed to extract time from field")
|
||||||
|
}
|
||||||
|
|
||||||
|
ti, err := time.Parse(time.RFC3339, t)
|
||||||
|
if err != nil {
|
||||||
|
return metav1.Time{}, err
|
||||||
|
}
|
||||||
|
return metav1.Time{Time: ti}, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,29 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCMRender(t *testing.T) {
|
func TestCmRender(t *testing.T) {
|
||||||
c := render.ConfigMap{}
|
c := render.ConfigMap{}
|
||||||
r := render.NewRow(4)
|
r := render.NewRow(4)
|
||||||
c.Render(load(t, "cm"), "", &r)
|
|
||||||
|
|
||||||
|
assert.Nil(t, c.Render(load(t, "cm"), "", &r))
|
||||||
assert.Equal(t, "default/blee", r.ID)
|
assert.Equal(t, "default/blee", r.ID)
|
||||||
assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3])
|
assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkCmRender(b *testing.B) {
|
||||||
|
c := render.ConfigMap{}
|
||||||
|
r := render.NewRow(4)
|
||||||
|
o := load(b, "cm")
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = c.Render(o, "", &r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func load(t *testing.T, n string) *unstructured.Unstructured {
|
func load(t assert.TestingT, n string) *unstructured.Unstructured {
|
||||||
raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n))
|
raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected Deployment, but got %T", o)
|
return fmt.Errorf("Expected Deployment, but got %T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
var dp appsv1.Deployment
|
var dp appsv1.Deployment
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp)
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,23 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDeploymentRender(t *testing.T) {
|
func TestDpRender(t *testing.T) {
|
||||||
c := render.Deployment{}
|
c := render.Deployment{}
|
||||||
r := render.NewRow(7)
|
r := render.NewRow(7)
|
||||||
c.Render(load(t, "dp"), "", &r)
|
|
||||||
|
|
||||||
|
assert.Nil(t, c.Render(load(t, "dp"), "", &r))
|
||||||
assert.Equal(t, "icx/icx-db", r.ID)
|
assert.Equal(t, "icx/icx-db", r.ID)
|
||||||
assert.Equal(t, render.Fields{"icx", "icx-db", "1/1", "1", "1"}, r.Fields[:5])
|
assert.Equal(t, render.Fields{"icx", "icx-db", "1/1", "1", "1"}, r.Fields[:5])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkDpRender(b *testing.B) {
|
||||||
|
c := render.Deployment{}
|
||||||
|
r := render.NewRow(7)
|
||||||
|
o := load(b, "dp")
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = c.Render(o, "", &r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,13 +17,6 @@ const (
|
||||||
nodeLabelRole = "kubernetes.io/role"
|
nodeLabelRole = "kubernetes.io/role"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NodeWithMetrics represents a resourve object with usage metrics.
|
|
||||||
type NodeWithMetrics interface {
|
|
||||||
Object() runtime.Object
|
|
||||||
Metrics() *mv1beta1.NodeMetrics
|
|
||||||
Pods() []*v1.Pod
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node renders a K8s Node to screen.
|
// Node renders a K8s Node to screen.
|
||||||
type Node struct{}
|
type Node struct{}
|
||||||
|
|
||||||
|
|
@ -54,29 +47,33 @@ func (Node) Header(_ string) HeaderRow {
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (n Node) Render(o interface{}, ns string, r *Row) error {
|
func (n Node) Render(o interface{}, ns string, r *Row) error {
|
||||||
oo, ok := o.(NodeWithMetrics)
|
oo, ok := o.(*NodeWithMetrics)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected NodeAndMetrics, but got %T", o)
|
return fmt.Errorf("Expected *NodeAndMetrics, but got %T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
meta, ok := oo.Raw.Object["metadata"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Unable to extract meta")
|
||||||
|
}
|
||||||
|
na := extractMetaField(meta, "name")
|
||||||
var no v1.Node
|
var no v1.Node
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &no)
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Raw.Object, &no)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Converting Node")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
iIP, eIP := getIPs(no.Status.Addresses)
|
iIP, eIP := getIPs(no.Status.Addresses)
|
||||||
iIP, eIP = missing(iIP), missing(eIP)
|
iIP, eIP = missing(iIP), missing(eIP)
|
||||||
|
|
||||||
c, a, p := gatherNodeMX(&no, oo.Metrics())
|
c, a, p := gatherNodeMX(&no, oo.MX)
|
||||||
|
|
||||||
sta := make([]string, 10)
|
sta := make([]string, 10)
|
||||||
status(no.Status, no.Spec.Unschedulable, sta)
|
status(no.Status, no.Spec.Unschedulable, sta)
|
||||||
ro := make([]string, 10)
|
ro := make([]string, 10)
|
||||||
nodeRoles(&no, ro)
|
nodeRoles(&no, ro)
|
||||||
|
|
||||||
r.ID = MetaFQN(no.ObjectMeta)
|
r.ID = FQN("", na)
|
||||||
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
r.Fields = make(Fields, 0, len(n.Header(ns)))
|
||||||
r.Fields = append(r.Fields,
|
r.Fields = append(r.Fields,
|
||||||
no.Name,
|
no.Name,
|
||||||
|
|
@ -101,6 +98,22 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
// NodeWithMetrics represents a node with its associated metrics.
|
||||||
|
type NodeWithMetrics struct {
|
||||||
|
Raw *unstructured.Unstructured
|
||||||
|
MX *mv1beta1.NodeMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (n *NodeWithMetrics) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (n *NodeWithMetrics) DeepCopyObject() runtime.Object {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c metric, a metric, p metric) {
|
func gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c metric, a metric, p metric) {
|
||||||
c, a, p = noMetric(), noMetric(), noMetric()
|
c, a, p = noMetric(), noMetric(), noMetric()
|
||||||
if mx == nil {
|
if mx == nil {
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,19 @@ import (
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNodeRender(t *testing.T) {
|
func TestNodeRender(t *testing.T) {
|
||||||
pom := nodeMetrics{
|
pom := render.NodeWithMetrics{
|
||||||
load(t, "no"),
|
Raw: load(t, "no"),
|
||||||
makeNodeMX("n1", "10m", "10Mi"),
|
MX: makeNodeMX("n1", "10m", "10Mi"),
|
||||||
[]*v1.Pod{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var no render.Node
|
var no render.Node
|
||||||
r := render.NewRow(14)
|
r := render.NewRow(14)
|
||||||
err := no.Render(pom, "", &r)
|
err := no.Render(&pom, "", &r)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "minikube", r.ID)
|
assert.Equal(t, "minikube", r.ID)
|
||||||
|
|
@ -29,27 +25,23 @@ func TestNodeRender(t *testing.T) {
|
||||||
assert.Equal(t, e, r.Fields[:13])
|
assert.Equal(t, e, r.Fields[:13])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkNodeRender(b *testing.B) {
|
||||||
|
pom := render.NodeWithMetrics{
|
||||||
|
Raw: load(b, "no"),
|
||||||
|
MX: makeNodeMX("n1", "10m", "10Mi"),
|
||||||
|
}
|
||||||
|
var no render.Node
|
||||||
|
r := render.NewRow(14)
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = no.Render(&pom, "", &r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
type nodeMetrics struct {
|
|
||||||
o *unstructured.Unstructured
|
|
||||||
m *mv1beta1.NodeMetrics
|
|
||||||
pod []*v1.Pod
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p nodeMetrics) Object() runtime.Object {
|
|
||||||
return p.o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p nodeMetrics) Metrics() *mv1beta1.NodeMetrics {
|
|
||||||
return p.m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p nodeMetrics) Pods() []*v1.Pod {
|
|
||||||
return p.pod
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeNodeMX(name, cpu, mem string) *mv1beta1.NodeMetrics {
|
func makeNodeMX(name, cpu, mem string) *mv1beta1.NodeMetrics {
|
||||||
return &mv1beta1.NodeMetrics{
|
return &mv1beta1.NodeMetrics{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,15 @@ import (
|
||||||
|
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/kubernetes/pkg/util/node"
|
"k8s.io/kubernetes/pkg/util/node"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PodWithMetrics represents a resourve object with usage metrics.
|
|
||||||
type PodWithMetrics interface {
|
|
||||||
Object() runtime.Object
|
|
||||||
Metrics() *mv1beta1.PodMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pod renders a K8s Pod to screen.
|
// Pod renders a K8s Pod to screen.
|
||||||
type Pod struct{}
|
type Pod struct{}
|
||||||
|
|
||||||
|
|
@ -94,21 +88,20 @@ func (Pod) Header(ns string) HeaderRow {
|
||||||
|
|
||||||
// Render renders a K8s resource to screen.
|
// Render renders a K8s resource to screen.
|
||||||
func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
oo, ok := o.(PodWithMetrics)
|
oo, ok := o.(*PodWithMetrics)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Expected PodAndMetrics, but got %T", o)
|
return fmt.Errorf("Expected PodWithMetrics, but got %T", o)
|
||||||
}
|
}
|
||||||
|
|
||||||
var po v1.Pod
|
var po v1.Pod
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Object().(*unstructured.Unstructured).Object, &po)
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Raw.Object, &po)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Expecting a pod resource")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ss := po.Status.ContainerStatuses
|
ss := po.Status.ContainerStatuses
|
||||||
cr, _, rc := p.statuses(ss)
|
cr, _, rc := p.statuses(ss)
|
||||||
c, perc := p.gatherPodMX(&po, oo.Metrics())
|
c, perc := p.gatherPodMX(&po, oo.MX)
|
||||||
|
|
||||||
r.ID = MetaFQN(po.ObjectMeta)
|
r.ID = MetaFQN(po.ObjectMeta)
|
||||||
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
r.Fields = make(Fields, 0, len(p.Header(ns)))
|
||||||
|
|
@ -136,6 +129,22 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
|
// PodWithMetrics represents a pod and its metrics.
|
||||||
|
type PodWithMetrics struct {
|
||||||
|
Raw *unstructured.Unstructured
|
||||||
|
MX *mv1beta1.PodMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectKind returns a schema object.
|
||||||
|
func (p *PodWithMetrics) GetObjectKind() schema.ObjectKind {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject returns a container copy.
|
||||||
|
func (p *PodWithMetrics) DeepCopyObject() runtime.Object {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) {
|
func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) {
|
||||||
c, p = noMetric(), noMetric()
|
c, p = noMetric(), noMetric()
|
||||||
if mx == nil {
|
if mx == nil {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ import (
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
res "k8s.io/apimachinery/pkg/api/resource"
|
res "k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -61,11 +59,14 @@ func TestPodColorer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPodRender(t *testing.T) {
|
func TestPodRender(t *testing.T) {
|
||||||
pom := podMetrics{load(t, "po"), makePodMX("nginx", "10m", "10Mi")}
|
pom := render.PodWithMetrics{
|
||||||
|
Raw: load(t, "po"),
|
||||||
|
MX: makePodMX("nginx", "10m", "10Mi"),
|
||||||
|
}
|
||||||
|
|
||||||
var po render.Pod
|
var po render.Pod
|
||||||
r := render.NewRow(12)
|
r := render.NewRow(12)
|
||||||
err := po.Render(pom, "", &r)
|
err := po.Render(&pom, "", &r)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "default/nginx", r.ID)
|
assert.Equal(t, "default/nginx", r.ID)
|
||||||
|
|
@ -73,12 +74,30 @@ func TestPodRender(t *testing.T) {
|
||||||
assert.Equal(t, e, r.Fields[:12])
|
assert.Equal(t, e, r.Fields[:12])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkPodRender(b *testing.B) {
|
||||||
|
pom := render.PodWithMetrics{
|
||||||
|
Raw: load(b, "po"),
|
||||||
|
MX: makePodMX("nginx", "10m", "10Mi"),
|
||||||
|
}
|
||||||
|
var po render.Pod
|
||||||
|
r := render.NewRow(12)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = po.Render(&pom, "", &r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPodInitRender(t *testing.T) {
|
func TestPodInitRender(t *testing.T) {
|
||||||
pom := podMetrics{load(t, "po_init"), makePodMX("nginx", "10m", "10Mi")}
|
pom := render.PodWithMetrics{
|
||||||
|
Raw: load(t, "po_init"),
|
||||||
|
MX: makePodMX("nginx", "10m", "10Mi"),
|
||||||
|
}
|
||||||
|
|
||||||
var po render.Pod
|
var po render.Pod
|
||||||
r := render.NewRow(12)
|
r := render.NewRow(12)
|
||||||
err := po.Render(pom, "", &r)
|
err := po.Render(&pom, "", &r)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "default/nginx", r.ID)
|
assert.Equal(t, "default/nginx", r.ID)
|
||||||
|
|
@ -89,19 +108,6 @@ func TestPodInitRender(t *testing.T) {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
type podMetrics struct {
|
|
||||||
o *unstructured.Unstructured
|
|
||||||
m *mv1beta1.PodMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p podMetrics) Object() runtime.Object {
|
|
||||||
return p.o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p podMetrics) Metrics() *mv1beta1.PodMetrics {
|
|
||||||
return p.m
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePodMX(name, cpu, mem string) *mv1beta1.PodMetrics {
|
func makePodMX(name, cpu, mem string) *mv1beta1.PodMetrics {
|
||||||
return &mv1beta1.PodMetrics{
|
return &mv1beta1.PodMetrics{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,6 @@ func (l *LogsExtender) showLogs(path string, prev bool) {
|
||||||
l.App().Flash().Err(err)
|
l.App().Flash().Err(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.App().factory.WaitForCacheSync()
|
|
||||||
|
|
||||||
co := ""
|
co := ""
|
||||||
if l.containerFn != nil {
|
if l.containerFn != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/gdamore/tcell"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,6 +23,7 @@ func NewNode(gvr client.GVR) ResourceViewer {
|
||||||
}
|
}
|
||||||
n.SetBindKeysFn(n.bindKeys)
|
n.SetBindKeysFn(n.bindKeys)
|
||||||
n.GetTable().SetEnterFn(n.showPods)
|
n.GetTable().SetEnterFn(n.showPods)
|
||||||
|
n.SetContextFn(n.nodeContext)
|
||||||
|
|
||||||
return &n
|
return &n
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +39,16 @@ func (n *Node) bindKeys(aa ui.KeyActions) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Node) nodeContext(ctx context.Context) context.Context {
|
||||||
|
mx := client.NewMetricsServer(n.App().factory.Client())
|
||||||
|
nmx, err := mx.FetchNodesMetrics()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("No node metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.WithValue(ctx, internal.KeyMetrics, nmx)
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Node) showPods(app *App, ns, res, sel string) {
|
func (n *Node) showPods(app *App, ns, res, sel string) {
|
||||||
showPods(app, n.GetTable().GetSelectedItem(), "", "spec.nodeName="+sel)
|
showPods(app, n.GetTable().GetSelectedItem(), "", "spec.nodeName="+sel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ func NewPod(gvr client.GVR) ResourceViewer {
|
||||||
p.SetBindKeysFn(p.bindKeys)
|
p.SetBindKeysFn(p.bindKeys)
|
||||||
p.GetTable().SetEnterFn(p.showContainers)
|
p.GetTable().SetEnterFn(p.showContainers)
|
||||||
p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc())
|
p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc())
|
||||||
|
p.SetContextFn(p.podMXContext)
|
||||||
|
|
||||||
return &p
|
return &p
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +53,21 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pod) podMXContext(ctx context.Context) context.Context {
|
||||||
|
ns, ok := ctx.Value(internal.KeyNamespace).(string)
|
||||||
|
if !ok {
|
||||||
|
log.Error().Err(fmt.Errorf("Expecting context namespace"))
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("POD METRICS in NS %q", ns)
|
||||||
|
mx := client.NewMetricsServer(p.App().factory.Client())
|
||||||
|
nmx, err := mx.FetchPodsMetrics(ns)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msgf("No pods metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.WithValue(ctx, internal.KeyMetrics, nmx)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pod) showContainers(app *App, ns, gvr, path string) {
|
func (p *Pod) showContainers(app *App, ns, gvr, path string) {
|
||||||
log.Debug().Msgf("SHOW CONTAINERS %q -- %q -- %q", gvr, ns, path)
|
log.Debug().Msgf("SHOW CONTAINERS %q -- %q -- %q", gvr, ns, path)
|
||||||
co := NewContainer(client.NewGVR("containers"))
|
co := NewContainer(client.NewGVR("containers"))
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func (f *Factory) Terminate() {
|
||||||
// List returns a resource collection.
|
// List returns a resource collection.
|
||||||
func (f *Factory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
|
func (f *Factory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
|
||||||
defer func(t time.Time) {
|
defer func(t time.Time) {
|
||||||
log.Debug().Msgf("LIST time %v", time.Since(t))
|
log.Debug().Msgf("LIST elapsed %v", time.Since(t))
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
Dump(f)
|
Dump(f)
|
||||||
|
|
@ -85,7 +85,7 @@ func (f *Factory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtim
|
||||||
// Get retrieves a given resource.
|
// Get retrieves a given resource.
|
||||||
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
|
||||||
defer func(t time.Time) {
|
defer func(t time.Time) {
|
||||||
log.Debug().Msgf("GET time %v", time.Since(t))
|
log.Debug().Msgf("GET elapsed %v", time.Since(t))
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
ns, n := namespaced(path)
|
ns, n := namespaced(path)
|
||||||
|
|
@ -105,7 +105,15 @@ func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime
|
||||||
|
|
||||||
func (f *Factory) waitForCacheSync(ns string) {
|
func (f *Factory) waitForCacheSync(ns string) {
|
||||||
if fac, ok := f.factories[ns]; ok {
|
if fac, ok := f.factories[ns]; ok {
|
||||||
fac.WaitForCacheSync(f.stopChan)
|
// Hang for a sec for the cache to refresh if still not done bail out!
|
||||||
|
const dur = 1 * time.Second
|
||||||
|
c := make(chan struct{})
|
||||||
|
go func(c chan struct{}) {
|
||||||
|
<-time.After(dur)
|
||||||
|
log.Warn().Msgf("Wait for sync timed out!")
|
||||||
|
close(c)
|
||||||
|
}(c)
|
||||||
|
fac.WaitForCacheSync(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue