perf pass on no/po/co

mine
derailed 2020-11-06 14:22:02 -07:00
parent a6f950b2a2
commit ea9f6abd08
14 changed files with 323 additions and 210 deletions

View File

@ -25,7 +25,7 @@ const (
var _ config.KubeSettings = (*client.Config)(nil) var _ config.KubeSettings = (*client.Config)(nil)
var ( var (
version, commit, date = "dev", "dev", client.NA version, commit, date = "0.23.8", "dev", client.NA
k9sFlags *config.Flags k9sFlags *config.Flags
k8sFlags *genericclioptions.ConfigFlags k8sFlags *genericclioptions.ConfigFlags

2
go.sum
View File

@ -855,6 +855,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
helm.sh/helm v1.2.1 h1:Jrn7kKQqQ/hnFWZEX+9pMFvYqFexkzrBnGqYBmIph7c=
helm.sh/helm v2.17.0+incompatible h1:cSe3FaQOpRWLDXvTObQNj0P7WI98IG5yloU6tQVls2k=
helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE= helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE=
helm.sh/helm/v3 v3.2.0/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0= helm.sh/helm/v3 v3.2.0/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -59,32 +59,26 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
nodeMetrics := make(NodesMetrics, len(nos.Items)) nodeMetrics := make(NodesMetrics, len(nos.Items))
for _, no := range nos.Items { for _, no := range nos.Items {
nodeMetrics[no.Name] = NodeMetrics{ nodeMetrics[no.Name] = NodeMetrics{
AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(),
AllocatableMEM: no.Status.Allocatable.Memory().Value(), AllocatableMEM: no.Status.Allocatable.Memory().Value(),
AllocatableEphemeral: no.Status.Allocatable.StorageEphemeral().Value(),
} }
} }
for _, mx := range nmx.Items { for _, mx := range nmx.Items {
if node, ok := nodeMetrics[mx.Name]; ok { if node, ok := nodeMetrics[mx.Name]; ok {
node.CurrentCPU = mx.Usage.Cpu().MilliValue() node.CurrentCPU = mx.Usage.Cpu().MilliValue()
node.CurrentMEM = mx.Usage.Memory().Value() node.CurrentMEM = mx.Usage.Memory().Value()
node.CurrentEphemeral = mx.Usage.StorageEphemeral().Value()
nodeMetrics[mx.Name] = node nodeMetrics[mx.Name] = node
} }
} }
var ccpu, cmem, ceph, tcpu, tmem, teph int64 var ccpu, cmem, tcpu, tmem int64
for _, mx := range nodeMetrics { for _, mx := range nodeMetrics {
ccpu += mx.CurrentCPU ccpu += mx.CurrentCPU
cmem += mx.CurrentMEM cmem += mx.CurrentMEM
ceph += mx.CurrentEphemeral
tcpu += mx.AllocatableCPU tcpu += mx.AllocatableCPU
tmem += mx.AllocatableMEM tmem += mx.AllocatableMEM
teph += mx.AllocatableEphemeral
} }
mx.PercCPU = ToPercentage(ccpu, tcpu) mx.PercCPU, mx.PercMEM = ToPercentage(ccpu, tcpu), ToPercentage(cmem, tmem)
mx.PercMEM = ToPercentage(cmem, tmem)
mx.PercEphemeral = ToPercentage(ceph, teph)
return nil return nil
} }
@ -131,6 +125,22 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
} }
} }
// FetchNodesMetricsMap fetch node metrics as a map.
func (m *MetricsServer) FetchNodesMetricsMap(ctx context.Context) (NodesMetricsMap, error) {
mm, err := m.FetchNodesMetrics(ctx)
if err != nil {
return nil, err
}
hh := make(NodesMetricsMap, len(mm.Items))
for i := range mm.Items {
mx := mm.Items[i]
hh[mx.Name] = &mx
}
return hh, nil
}
// FetchNodesMetrics return all metrics for nodes. // FetchNodesMetrics return all metrics for nodes.
func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) { func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) {
const msg = "user is not authorized to list node metrics" const msg = "user is not authorized to list node metrics"
@ -162,6 +172,43 @@ func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMe
return mxList, nil return mxList, nil
} }
// FetchNodesMetrics return all metrics for nodes.
func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1beta1.NodeMetrics, error) {
const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetrics)
if err := m.checkAccess(ClusterScope, "metrics.k8s.io/v1beta1/nodes", msg); err != nil {
return mx, err
}
mmx, err := m.FetchNodesMetricsMap(ctx)
if err != nil {
return nil, err
}
mx, ok := mmx[n]
if !ok {
return nil, fmt.Errorf("Unable to retrieve node metrics for %q", n)
}
return mx, nil
}
// FetchPodsMetricsMap fetch pods metrics as a map.
func (m *MetricsServer) FetchPodsMetricsMap(ctx context.Context, ns string) (PodsMetricsMap, error) {
mm, err := m.FetchPodsMetrics(ctx, ns)
if err != nil {
return nil, err
}
hh := make(PodsMetricsMap, len(mm.Items))
for i := range mm.Items {
mx := mm.Items[i]
hh[FQN(mx.Namespace, mx.Name)] = &mx
}
return hh, nil
}
// FetchPodsMetrics return all metrics for pods in a given namespace. // FetchPodsMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) { func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) {
mx := new(mv1beta1.PodMetricsList) mx := new(mv1beta1.PodMetricsList)
@ -196,12 +243,27 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be
return mxList, err return mxList, err
} }
func (m *MetricsServer) FetchContainersMetrics(ctx context.Context, fqn string) (ContainersMetrics, error) {
mm, err := m.FetchPodMetrics(ctx, fqn)
if err != nil {
return nil, err
}
cmx := make(ContainersMetrics, len(mm.Containers))
for i := range mm.Containers {
c := mm.Containers[i]
cmx[c.Name] = &c
}
return cmx, nil
}
// FetchPodMetrics return all metrics for pods in a given namespace. // FetchPodMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1beta1.PodMetrics, error) { func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1beta1.PodMetrics, error) {
var mx *mv1beta1.PodMetrics var mx *mv1beta1.PodMetrics
const msg = "user is not authorized to list pod metrics" const msg = "user is not authorized to list pod metrics"
ns, n := Namespaced(fqn) ns, _ := Namespaced(fqn)
if ns == NamespaceAll { if ns == NamespaceAll {
ns = AllNamespaces ns = AllNamespaces
} }
@ -209,25 +271,16 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
return mx, err return mx, err
} }
if entry, ok := m.cache.Get(fqn); ok { mmx, err := m.FetchPodsMetricsMap(ctx, ns)
pmx, ok := entry.(*mv1beta1.PodMetrics) if err != nil {
if !ok { return nil, err
return nil, fmt.Errorf("expecting podmetrics but got %T", entry) }
} pmx, ok := mmx[fqn]
return pmx, nil if !ok {
return nil, fmt.Errorf("Unable to locate pod metrics for pod %q", fqn)
} }
client, err := m.MXDial() return pmx, nil
if err != nil {
return mx, err
}
mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(ctx, n, metav1.GetOptions{})
if err != nil {
return mx, err
}
m.cache.Add(fqn, mx, mxCacheExpiry)
return mx, nil
} }
// PodsMetrics retrieves metrics for all pods in a given namespace. // PodsMetrics retrieves metrics for all pods in a given namespace.

View File

@ -7,6 +7,7 @@ import (
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
versioned "k8s.io/metrics/pkg/client/clientset/versioned" versioned "k8s.io/metrics/pkg/client/clientset/versioned"
) )
@ -59,6 +60,15 @@ var (
ReadAllAccess = []string{GetVerb, ListVerb, WatchVerb} ReadAllAccess = []string{GetVerb, ListVerb, WatchVerb}
) )
// ContainersMetrics tracks containers metrics.
type ContainersMetrics map[string]*mv1beta1.ContainerMetrics
// NodesMetrics tracks node metrics.
type NodesMetricsMap map[string]*mv1beta1.NodeMetrics
// PodsMetrics tracks pod metrics.
type PodsMetricsMap map[string]*mv1beta1.PodMetrics
// Authorizer checks what a user can or cannot do to a resource. // Authorizer checks what a user can or cannot do to a resource.
type Authorizer interface { type Authorizer interface {
// CanI returns true if the user can use these actions for a given resource. // CanI returns true if the user can use these actions for a given resource.

View File

@ -7,7 +7,6 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"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/labels" "k8s.io/apimachinery/pkg/labels"
@ -33,13 +32,11 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
} }
var ( var (
pmx *mv1beta1.PodMetrics cmx client.ContainersMetrics
err error err error
) )
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(ctx, fqn); err != nil { cmx, _ = client.DialMetrics(c.Client()).FetchContainersMetrics(ctx, fqn)
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
}
} }
po, err := c.fetchPod(fqn) po, err := c.fetchPod(fqn)
@ -48,10 +45,10 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
} }
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)) res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
for _, co := range po.Spec.InitContainers { for _, co := range po.Spec.InitContainers {
res = append(res, makeContainerRes(co, po, pmx, true)) res = append(res, makeContainerRes(co, po, cmx[co.Name], true))
} }
for _, co := range po.Spec.Containers { for _, co := range po.Spec.Containers {
res = append(res, makeContainerRes(co, po, pmx, false)) res = append(res, makeContainerRes(co, po, cmx[co.Name], false))
} }
return res, nil return res, nil
@ -68,12 +65,7 @@ func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptio
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Helpers... // Helpers...
func makeContainerRes(co v1.Container, po *v1.Pod, pmx *mv1beta1.PodMetrics, isInit bool) render.ContainerRes { func makeContainerRes(co v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics, isInit bool) render.ContainerRes {
cmx, err := containerMetrics(co.Name, pmx)
if err != nil {
log.Warn().Err(err).Msgf("No container metrics found for %s::%s", po.Name, co.Name)
}
return render.ContainerRes{ return render.ContainerRes{
Container: &co, Container: &co,
Status: getContainerStatus(co.Name, po.Status), Status: getContainerStatus(co.Name, po.Status),
@ -83,18 +75,6 @@ func makeContainerRes(co v1.Container, po *v1.Pod, pmx *mv1beta1.PodMetrics, isI
} }
} }
func containerMetrics(n string, pmx *mv1beta1.PodMetrics) (*mv1beta1.ContainerMetrics, error) {
if pmx == nil {
return nil, fmt.Errorf("no metrics for container %s", n)
}
for _, m := range pmx.Containers {
if m.Name == n {
return &m, nil
}
}
return nil, nil
}
func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus { func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
for _, c := range status.ContainerStatuses { for _, c := range status.ContainerStatuses {
if c.Name == co { if c.Name == co {
@ -117,9 +97,5 @@ func (c *Container) fetchPod(fqn string) (*v1.Pod, error) {
} }
var po v1.Pod var po v1.Pod
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po) err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po)
if err != nil { return &po, err
return nil, err
}
return &po, nil
} }

View File

@ -10,7 +10,6 @@ import (
"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" 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/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -110,66 +109,58 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
// Get returns a node resource. // Get returns a node resource.
func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) { func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) {
var ( o, err := n.Resource.Get(ctx, path)
nmx *mv1beta1.NodeMetricsList if err != nil {
err error return o, err
) }
u, ok := o.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
}
var nmx *mv1beta1.NodeMetrics
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
nmx, _ = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx) nmx, _ = client.DialMetrics(n.Client()).FetchNodeMetrics(ctx, path)
} }
no, err := FetchNode(ctx, n.Factory, path) return &render.NodeWithMetrics{Raw: u, MX: nmx}, nil
if err != nil {
return nil, err
}
o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&no)
if err != nil {
return nil, err
}
return &render.NodeWithMetrics{
Raw: &unstructured.Unstructured{Object: o},
MX: nodeMetricsFor(MetaFQN(no.ObjectMeta), nmx),
}, nil
} }
// List returns a collection of node resources. // List returns a collection of node resources.
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
var (
nmx *mv1beta1.NodeMetricsList
err error
)
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
nmx, _ = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx)
}
labels, _ := ctx.Value(internal.KeyLabels).(string) oo, err := n.Resource.List(ctx, ns)
nn, err := FetchNodes(ctx, n.Factory, labels)
if err != nil { if err != nil {
return nil, err return oo, err
}
oo := make([]runtime.Object, len(nn.Items))
for i, no := range nn.Items {
o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&nn.Items[i])
if err != nil {
return nil, err
}
meta, ok := o["metadata"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("expecting interface map but got `%T", o)
}
pods, err := n.GetPods(meta["name"].(string))
if err != nil {
return nil, err
}
oo[i] = &render.NodeWithMetrics{
Raw: &unstructured.Unstructured{Object: o},
MX: nodeMetricsFor(MetaFQN(no.ObjectMeta), nmx),
Pods: pods,
}
} }
return oo, nil var nmx client.NodesMetricsMap
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
nmx, _ = client.DialMetrics(n.Client()).FetchNodesMetricsMap(ctx)
}
res := make([]runtime.Object, 0, len(oo))
for _, o := range oo {
u, ok := o.(*unstructured.Unstructured)
if !ok {
return res, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
}
fqn := extractFQN(o)
_, name := client.Namespaced(fqn)
podCount, err := n.CountPods(name)
if err != nil {
return nil, err
}
res = append(res, &render.NodeWithMetrics{
Raw: u,
MX: nmx[name],
PodCount: podCount,
})
}
return res, nil
} }
// CountPods counts the pods scheduled on a given node. // CountPods counts the pods scheduled on a given node.
@ -189,7 +180,7 @@ func (n *Node) CountPods(nodeName string) (int, error) {
if !ok { if !ok {
return count, fmt.Errorf("expecting interface map but got `%T", o) return count, fmt.Errorf("expecting interface map but got `%T", o)
} }
if spec["nodeName"] == nodeName { if node, ok := spec["nodeName"]; ok && node == nodeName {
count++ count++
} }
} }
@ -231,11 +222,18 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
return nil, fmt.Errorf("user is not authorized to list nodes") return nil, fmt.Errorf("user is not authorized to list nodes")
} }
dial, err := f.Client().Dial() o, err := f.Get("v1/nodes", path, false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return dial.CoreV1().Nodes().Get(ctx, path, metav1.GetOptions{})
var node v1.Node
err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &node)
if err != nil {
return nil, err
}
return &node, nil
} }
// FetchNodes retrieves all nodes. // FetchNodes retrieves all nodes.
@ -248,23 +246,19 @@ func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList,
return nil, fmt.Errorf("user is not authorized to list nodes") return nil, fmt.Errorf("user is not authorized to list nodes")
} }
dial, err := f.Client().Dial() oo, err := f.List("v1/nodes", "", false, labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return dial.CoreV1().Nodes().List(ctx, metav1.ListOptions{ nn := make([]v1.Node, 0, len(oo))
LabelSelector: labelsSel, for _, o := range oo {
}) var node v1.Node
} err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &node)
if err != nil {
func nodeMetricsFor(fqn string, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics { return nil, err
if mmx == nil {
return nil
}
for _, mx := range mmx.Items {
if MetaFQN(mx.ObjectMeta) == fqn {
return &mx
} }
nn = append(nn, node)
} }
return nil
return &v1.NodeList{Items: nn}, nil
} }

View File

@ -73,6 +73,15 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
// List returns a collection of nodes. // List returns a collection of nodes.
func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
oo, err := p.Resource.List(ctx, ns)
if err != nil {
return oo, err
}
var pmx client.PodsMetricsMap
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
pmx, _ = client.DialMetrics(p.Client()).FetchPodsMetricsMap(ctx, ns)
}
sel, _ := ctx.Value(internal.KeyFields).(string) sel, _ := ctx.Value(internal.KeyFields).(string)
fsel, err := labels.ConvertSelectorToLabelsMap(sel) fsel, err := labels.ConvertSelectorToLabelsMap(sel)
if err != nil { if err != nil {
@ -80,24 +89,15 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
} }
nodeName := fsel["spec.nodeName"] nodeName := fsel["spec.nodeName"]
oo, err := p.Resource.List(ctx, ns)
if err != nil {
return oo, err
}
var pmx *mv1beta1.PodMetricsList
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
pmx, _ = client.DialMetrics(p.Client()).FetchPodsMetrics(ctx, ns)
}
res := make([]runtime.Object, 0, len(oo)) res := make([]runtime.Object, 0, len(oo))
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.Unstructured but got `%T", o) return res, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
} }
fqn := extractFQN(o)
if nodeName == "" { if nodeName == "" {
res = append(res, &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)}) res = append(res, &render.PodWithMetrics{Raw: u, MX: pmx[fqn]})
continue continue
} }
@ -106,7 +106,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
return res, fmt.Errorf("expecting interface map but got `%T", o) return res, fmt.Errorf("expecting interface map but got `%T", o)
} }
if spec["nodeName"] == nodeName { if spec["nodeName"] == nodeName {
res = append(res, &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)}) res = append(res, &render.PodWithMetrics{Raw: u, MX: pmx[fqn]})
} }
} }
@ -370,7 +370,7 @@ func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
for { for {
bytes, err := r.ReadBytes('\n') bytes, err := r.ReadBytes('\n')
if err != nil { if err != nil {
if err == io.EOF { if errors.Is(err, io.EOF) {
log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info()) log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info())
c <- opts.DecorateLog([]byte("\nlog stream closed\n")) c <- opts.DecorateLog([]byte("\nlog stream closed\n"))
return return
@ -383,19 +383,6 @@ func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
} }
} }
func podMetricsFor(o runtime.Object, mmx *mv1beta1.PodMetricsList) *mv1beta1.PodMetrics {
if mmx == nil {
return nil
}
fqn := extractFQN(o)
for _, mx := range mmx.Items {
if MetaFQN(mx.ObjectMeta) == fqn {
return &mx
}
}
return nil
}
// MetaFQN returns a fully qualified resource name. // MetaFQN returns a fully qualified resource name.
func MetaFQN(m metav1.ObjectMeta) string { func MetaFQN(m metav1.ObjectMeta) string {
if m.Namespace == "" { if m.Namespace == "" {

View File

@ -90,23 +90,22 @@ func (c *Cluster) UserName() string {
// Metrics gathers node level metrics and compute utilization percentages. // Metrics gathers node level metrics and compute utilization percentages.
func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error { func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error {
var nn *v1.NodeList var (
if n, ok := c.cache.Get(clusterNodesKey); ok { nn *v1.NodeList
if nodes, ok := n.(*v1.NodeList); ok { err error
nn = nodes )
} if v, ok := c.cache.Get(clusterNodesKey); ok {
} nn = v.(*v1.NodeList)
} else {
var err error if nn, err = dao.FetchNodes(ctx, c.factory, ""); err != nil {
if nn == nil {
nn, err = dao.FetchNodes(ctx, c.factory, "")
if err != nil {
return err return err
} }
} }
c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry) if len(nn.Items) > 0 {
nmx, err := c.mx.FetchNodesMetrics(ctx) c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry)
if err != nil { }
var nmx *mv1beta1.NodeMetricsList
if nmx, err = c.mx.FetchNodesMetrics(ctx); err != nil {
return err return err
} }

View File

@ -12,7 +12,6 @@ import (
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/util/cache"
"vbom.ml/util/sortorder"
) )
const ( const (
@ -101,7 +100,7 @@ func (c *ClusterInfo) fetchK9sLatestRev() string {
return rev.(string) return rev.(string)
} }
latestRev, err := checkLastestRev() latestRev, err := fetchLastestRev()
if err != nil { if err != nil {
log.Error().Msgf("k9s latest rev fetch failed") log.Error().Msgf("k9s latest rev fetch failed")
} else { } else {
@ -123,8 +122,9 @@ func (c *ClusterInfo) Refresh() {
data.Context = c.cluster.ContextName() data.Context = c.cluster.ContextName()
data.Cluster = c.cluster.ClusterName() data.Cluster = c.cluster.ClusterName()
data.User = c.cluster.UserName() data.User = c.cluster.UserName()
data.K9sVer, data.K9sLatest = c.version, c.fetchK9sLatestRev() v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev())
if !sortorder.NaturalLess(data.K9sVer, data.K9sLatest) { data.K9sVer, data.K9sLatest = v1.String(), v2.String()
if v1.IsCurrent(v2) {
data.K9sLatest = "" data.K9sLatest = ""
} }
data.K8sVer = c.cluster.Version() data.K8sVer = c.cluster.Version()
@ -134,6 +134,8 @@ func (c *ClusterInfo) Refresh() {
var mx client.ClusterMetrics var mx client.ClusterMetrics
if err := c.cluster.Metrics(ctx, &mx); err == nil { if err := c.cluster.Metrics(ctx, &mx); err == nil {
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
} else {
log.Error().Err(err).Msgf("Cluster metrics failed")
} }
if c.data.Deltas(data) { if c.data.Deltas(data) {
@ -178,7 +180,7 @@ func (c *ClusterInfo) fireNoMetaChanged(data ClusterMeta) {
// Helpers... // Helpers...
func checkLastestRev() (string, error) { func fetchLastestRev() (string, error) {
log.Debug().Msgf("Fetching latest k9s rev...") log.Debug().Msgf("Fetching latest k9s rev...")
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() defer cancel()

View File

@ -6,41 +6,12 @@ import (
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"vbom.ml/util/sortorder"
) )
func init() { func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel) zerolog.SetGlobalLevel(zerolog.FatalLevel)
} }
func TestVersionCheck(t *testing.T) {
uu := map[string]struct {
current, latest string
e bool
}{
"same": {
current: "v0.11.1",
latest: "v0.11.1",
},
"updated": {
current: "v0.11.1",
latest: "v0.12.1",
e: true,
},
"current": {
current: "v0.11.1",
latest: "v0.09.2",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, sortorder.NaturalLess(u.current, u.latest))
})
}
}
func TestClusterMetaDelta(t *testing.T) { func TestClusterMetaDelta(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
o, n model.ClusterMeta o, n model.ClusterMeta

52
internal/model/semver.go Normal file
View File

@ -0,0 +1,52 @@
package model
import (
"fmt"
"regexp"
"strconv"
)
var versionRX = regexp.MustCompile(`\Av(\d+)\.(\d+)\.(\d+)\z`)
// SemVer represents a semantic version.
type SemVer struct {
Major, Minor, Patch int
}
// NewSemVer returns a new semantic version.
func NewSemVer(version string) *SemVer {
var v SemVer
v.Major, v.Minor, v.Patch = v.parse(NormalizeVersion(version))
return &v
}
// String returns version as a string.
func (v *SemVer) String() string {
return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch)
}
func (*SemVer) parse(version string) (major, minor, patch int) {
mm := versionRX.FindStringSubmatch(version)
if len(mm) < 4 {
return
}
major, _ = strconv.Atoi(mm[1])
minor, _ = strconv.Atoi(mm[2])
patch, _ = strconv.Atoi(mm[3])
return
}
// Normalize ensures the version starts with a v.
func NormalizeVersion(version string) string {
if version[0] == 'v' {
return version
}
return "v" + version
}
// IsCurrent asserts if at latest release.
func (v *SemVer) IsCurrent(latest *SemVer) bool {
return v.Major >= latest.Major && v.Minor >= latest.Minor && v.Patch >= latest.Patch
}

View File

@ -0,0 +1,68 @@
package model_test
import (
"testing"
"github.com/derailed/k9s/internal/model"
"github.com/stretchr/testify/assert"
)
func TestNewSemVer(t *testing.T) {
uu := map[string]struct {
version string
major, minor, patch int
}{
"plain": {
version: "0.11.1",
major: 0,
minor: 11,
patch: 1,
},
"normalized": {
version: "v10.11.12",
major: 10,
minor: 11,
patch: 12,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
v := model.NewSemVer(u.version)
assert.Equal(t, u.major, v.Major)
assert.Equal(t, u.minor, v.Minor)
assert.Equal(t, u.patch, v.Patch)
})
}
}
func TestSemVerIsCurrent(t *testing.T) {
uu := map[string]struct {
current, latest string
e bool
}{
"same": {
current: "0.11.1",
latest: "0.11.1",
e: true,
},
"older": {
current: "v10.11.12",
latest: "v10.11.13",
},
"newer": {
current: "10.11.13",
latest: "10.11.12",
e: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
v1, v2 := model.NewSemVer(u.current), model.NewSemVer(u.latest)
assert.Equal(t, u.e, v1.IsCurrent(v2))
})
}
}

View File

@ -89,7 +89,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
no.Status.NodeInfo.KernelVersion, no.Status.NodeInfo.KernelVersion,
iIP, iIP,
eIP, eIP,
strconv.Itoa(len(oo.Pods)), strconv.Itoa(oo.PodCount),
toMc(c.cpu), toMc(c.cpu),
toMi(c.mem), toMi(c.mem),
strconv.Itoa(p.rCPU()), strconv.Itoa(p.rCPU()),
@ -134,9 +134,9 @@ func (Node) diagnose(ss []string) error {
// NodeWithMetrics represents a node with its associated metrics. // NodeWithMetrics represents a node with its associated metrics.
type NodeWithMetrics struct { type NodeWithMetrics struct {
Raw *unstructured.Unstructured Raw *unstructured.Unstructured
MX *mv1beta1.NodeMetrics MX *mv1beta1.NodeMetrics
Pods []*v1.Pod PodCount int
} }
// GetObjectKind returns a schema object. // GetObjectKind returns a schema object.

View File

@ -73,7 +73,7 @@ func (a *App) ConOK() bool {
// Init initializes the application. // Init initializes the application.
func (a *App) Init(version string, rate int) error { func (a *App) Init(version string, rate int) error {
a.version = version a.version = model.NormalizeVersion(version)
ctx := context.WithValue(context.Background(), internal.KeyApp, a) ctx := context.WithValue(context.Background(), internal.KeyApp, a)
if err := a.Content.Init(ctx); err != nil { if err := a.Content.Init(ctx); err != nil {
@ -103,7 +103,7 @@ func (a *App) Init(version string, rate int) error {
} }
a.initFactory(ns) a.initFactory(ns)
a.clusterModel = model.NewClusterInfo(a.factory, version) a.clusterModel = model.NewClusterInfo(a.factory, a.version)
a.clusterModel.AddListener(a.clusterInfo()) a.clusterModel.AddListener(a.clusterInfo())
a.clusterModel.AddListener(a.statusIndicator()) a.clusterModel.AddListener(a.statusIndicator())
a.clusterModel.Refresh() a.clusterModel.Refresh()
@ -115,13 +115,13 @@ func (a *App) Init(version string, rate int) error {
} }
a.CmdBuff().SetSuggestionFn(a.suggestCommand()) a.CmdBuff().SetSuggestionFn(a.suggestCommand())
a.layout(ctx, version) a.layout(ctx)
a.initSignals() a.initSignals()
return nil return nil
} }
func (a *App) layout(ctx context.Context, version string) { func (a *App) layout(ctx context.Context) {
flash := ui.NewFlash(a.App) flash := ui.NewFlash(a.App)
go flash.Watch(ctx, a.Flash().Channel()) go flash.Watch(ctx, a.Flash().Channel())
@ -134,9 +134,8 @@ func (a *App) layout(ctx context.Context, version string) {
main.AddItem(flash, 1, 1, false) main.AddItem(flash, 1, 1, false)
a.Main.AddPage("main", main, true, false) a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.Main.AddPage("splash", ui.NewSplash(a.Styles, a.version), true, true)
a.toggleHeader(!a.Config.K9s.IsHeadless()) a.toggleHeader(!a.Config.K9s.IsHeadless())
// a.toggleCrumbs(!a.Config.K9s.GetCrumbsless())
} }
func (a *App) initSignals() { func (a *App) initSignals() {