From ea9f6abd0834f693efe597a49ef85a267d4adeae Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 6 Nov 2020 14:22:02 -0700 Subject: [PATCH] perf pass on no/po/co --- cmd/root.go | 2 +- go.sum | 2 + internal/client/metrics.go | 109 +++++++++++++++++------ internal/client/types.go | 10 +++ internal/dao/container.go | 36 ++------ internal/dao/node.go | 128 +++++++++++++--------------- internal/dao/pod.go | 39 +++------ internal/model/cluster.go | 27 +++--- internal/model/cluster_info.go | 12 +-- internal/model/cluster_info_test.go | 29 ------- internal/model/semver.go | 52 +++++++++++ internal/model/semver_test.go | 68 +++++++++++++++ internal/render/node.go | 8 +- internal/view/app.go | 11 ++- 14 files changed, 323 insertions(+), 210 deletions(-) create mode 100644 internal/model/semver.go create mode 100644 internal/model/semver_test.go diff --git a/cmd/root.go b/cmd/root.go index e9a9323c..bec57785 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,7 +25,7 @@ const ( var _ config.KubeSettings = (*client.Config)(nil) var ( - version, commit, date = "dev", "dev", client.NA + version, commit, date = "0.23.8", "dev", client.NA k9sFlags *config.Flags k8sFlags *genericclioptions.ConfigFlags diff --git a/go.sum b/go.sum index 30da2e9b..078a5a1b 100644 --- a/go.sum +++ b/go.sum @@ -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= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 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/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/client/metrics.go b/internal/client/metrics.go index af8e37f0..4d7b050d 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -59,32 +59,26 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL nodeMetrics := make(NodesMetrics, len(nos.Items)) for _, no := range nos.Items { nodeMetrics[no.Name] = NodeMetrics{ - AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), - AllocatableMEM: no.Status.Allocatable.Memory().Value(), - AllocatableEphemeral: no.Status.Allocatable.StorageEphemeral().Value(), + AllocatableCPU: no.Status.Allocatable.Cpu().MilliValue(), + AllocatableMEM: no.Status.Allocatable.Memory().Value(), } } for _, mx := range nmx.Items { if node, ok := nodeMetrics[mx.Name]; ok { node.CurrentCPU = mx.Usage.Cpu().MilliValue() node.CurrentMEM = mx.Usage.Memory().Value() - node.CurrentEphemeral = mx.Usage.StorageEphemeral().Value() nodeMetrics[mx.Name] = node } } - var ccpu, cmem, ceph, tcpu, tmem, teph int64 + var ccpu, cmem, tcpu, tmem int64 for _, mx := range nodeMetrics { ccpu += mx.CurrentCPU cmem += mx.CurrentMEM - ceph += mx.CurrentEphemeral tcpu += mx.AllocatableCPU tmem += mx.AllocatableMEM - teph += mx.AllocatableEphemeral } - mx.PercCPU = ToPercentage(ccpu, tcpu) - mx.PercMEM = ToPercentage(cmem, tmem) - mx.PercEphemeral = ToPercentage(ceph, teph) + mx.PercCPU, mx.PercMEM = ToPercentage(ccpu, tcpu), ToPercentage(cmem, tmem) 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. func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) { 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 } +// 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. func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) { mx := new(mv1beta1.PodMetricsList) @@ -196,12 +243,27 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be 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. func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1beta1.PodMetrics, error) { var mx *mv1beta1.PodMetrics const msg = "user is not authorized to list pod metrics" - ns, n := Namespaced(fqn) + ns, _ := Namespaced(fqn) if ns == NamespaceAll { ns = AllNamespaces } @@ -209,25 +271,16 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be return mx, err } - if entry, ok := m.cache.Get(fqn); ok { - pmx, ok := entry.(*mv1beta1.PodMetrics) - if !ok { - return nil, fmt.Errorf("expecting podmetrics but got %T", entry) - } - return pmx, nil + mmx, err := m.FetchPodsMetricsMap(ctx, ns) + if err != nil { + return nil, err + } + pmx, ok := mmx[fqn] + if !ok { + return nil, fmt.Errorf("Unable to locate pod metrics for pod %q", fqn) } - client, err := m.MXDial() - 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 + return pmx, nil } // PodsMetrics retrieves metrics for all pods in a given namespace. diff --git a/internal/client/types.go b/internal/client/types.go index 149fa836..7838d5d7 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -7,6 +7,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" versioned "k8s.io/metrics/pkg/client/clientset/versioned" ) @@ -59,6 +60,15 @@ var ( 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. type Authorizer interface { // CanI returns true if the user can use these actions for a given resource. diff --git a/internal/dao/container.go b/internal/dao/container.go index 690422ec..20eacde7 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -7,7 +7,6 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" - "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -33,13 +32,11 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error } var ( - pmx *mv1beta1.PodMetrics + cmx client.ContainersMetrics err error ) if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { - if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(ctx, fqn); err != nil { - log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn) - } + cmx, _ = client.DialMetrics(c.Client()).FetchContainersMetrics(ctx, 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)) 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 { - res = append(res, makeContainerRes(co, po, pmx, false)) + res = append(res, makeContainerRes(co, po, cmx[co.Name], false)) } return res, nil @@ -68,12 +65,7 @@ func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptio // ---------------------------------------------------------------------------- // Helpers... -func makeContainerRes(co v1.Container, po *v1.Pod, pmx *mv1beta1.PodMetrics, 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) - } - +func makeContainerRes(co v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics, isInit bool) render.ContainerRes { return render.ContainerRes{ Container: &co, 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 { for _, c := range status.ContainerStatuses { if c.Name == co { @@ -117,9 +97,5 @@ func (c *Container) fetchPod(fqn string) (*v1.Pod, error) { } var po v1.Pod err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po) - if err != nil { - return nil, err - } - - return &po, nil + return &po, err } diff --git a/internal/dao/node.go b/internal/dao/node.go index 0985089d..46addddb 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -10,7 +10,6 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" 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/labels" "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. func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) { - var ( - nmx *mv1beta1.NodeMetricsList - err error - ) + o, err := n.Resource.Get(ctx, path) + if err != nil { + 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 { - nmx, _ = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx) + nmx, _ = client.DialMetrics(n.Client()).FetchNodeMetrics(ctx, path) } - no, err := FetchNode(ctx, n.Factory, path) - 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 + return &render.NodeWithMetrics{Raw: u, MX: nmx}, nil } // List returns a collection of node resources. 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) - nn, err := FetchNodes(ctx, n.Factory, labels) + oo, err := n.Resource.List(ctx, ns) if err != nil { - return nil, 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, err } - 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. @@ -189,7 +180,7 @@ func (n *Node) CountPods(nodeName string) (int, error) { if !ok { return count, fmt.Errorf("expecting interface map but got `%T", o) } - if spec["nodeName"] == nodeName { + if node, ok := spec["nodeName"]; ok && node == nodeName { 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") } - dial, err := f.Client().Dial() + o, err := f.Get("v1/nodes", path, false, labels.Everything()) if err != nil { 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. @@ -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") } - dial, err := f.Client().Dial() + oo, err := f.List("v1/nodes", "", false, labels.Everything()) if err != nil { return nil, err } - return dial.CoreV1().Nodes().List(ctx, metav1.ListOptions{ - LabelSelector: labelsSel, - }) -} - -func nodeMetricsFor(fqn string, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics { - if mmx == nil { - return nil - } - for _, mx := range mmx.Items { - if MetaFQN(mx.ObjectMeta) == fqn { - return &mx + nn := make([]v1.Node, 0, len(oo)) + for _, o := range oo { + var node v1.Node + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &node) + if err != nil { + return nil, err } + nn = append(nn, node) } - return nil + + return &v1.NodeList{Items: nn}, nil } diff --git a/internal/dao/pod.go b/internal/dao/pod.go index c3380c1e..5adbcd2b 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -73,6 +73,15 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) { // List returns a collection of nodes. 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) fsel, err := labels.ConvertSelectorToLabelsMap(sel) if err != nil { @@ -80,24 +89,15 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { } 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)) 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) if nodeName == "" { - res = append(res, &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)}) + res = append(res, &render.PodWithMetrics{Raw: u, MX: pmx[fqn]}) 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) } 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 { bytes, err := r.ReadBytes('\n') if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info()) c <- opts.DecorateLog([]byte("\nlog stream closed\n")) 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. func MetaFQN(m metav1.ObjectMeta) string { if m.Namespace == "" { diff --git a/internal/model/cluster.go b/internal/model/cluster.go index fe7beca5..62ef053f 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -90,23 +90,22 @@ func (c *Cluster) UserName() string { // Metrics gathers node level metrics and compute utilization percentages. func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error { - var nn *v1.NodeList - if n, ok := c.cache.Get(clusterNodesKey); ok { - if nodes, ok := n.(*v1.NodeList); ok { - nn = nodes - } - } - - var err error - if nn == nil { - nn, err = dao.FetchNodes(ctx, c.factory, "") - if err != nil { + var ( + nn *v1.NodeList + err error + ) + if v, ok := c.cache.Get(clusterNodesKey); ok { + nn = v.(*v1.NodeList) + } else { + if nn, err = dao.FetchNodes(ctx, c.factory, ""); err != nil { return err } } - c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry) - nmx, err := c.mx.FetchNodesMetrics(ctx) - if err != nil { + if len(nn.Items) > 0 { + c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry) + } + var nmx *mv1beta1.NodeMetricsList + if nmx, err = c.mx.FetchNodesMetrics(ctx); err != nil { return err } diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 6abe3349..3bc9c5a3 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -12,7 +12,6 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/util/cache" - "vbom.ml/util/sortorder" ) const ( @@ -101,7 +100,7 @@ func (c *ClusterInfo) fetchK9sLatestRev() string { return rev.(string) } - latestRev, err := checkLastestRev() + latestRev, err := fetchLastestRev() if err != nil { log.Error().Msgf("k9s latest rev fetch failed") } else { @@ -123,8 +122,9 @@ func (c *ClusterInfo) Refresh() { data.Context = c.cluster.ContextName() data.Cluster = c.cluster.ClusterName() data.User = c.cluster.UserName() - data.K9sVer, data.K9sLatest = c.version, c.fetchK9sLatestRev() - if !sortorder.NaturalLess(data.K9sVer, data.K9sLatest) { + v1, v2 := NewSemVer(data.K9sVer), NewSemVer(c.fetchK9sLatestRev()) + data.K9sVer, data.K9sLatest = v1.String(), v2.String() + if v1.IsCurrent(v2) { data.K9sLatest = "" } data.K8sVer = c.cluster.Version() @@ -134,6 +134,8 @@ func (c *ClusterInfo) Refresh() { var mx client.ClusterMetrics if err := c.cluster.Metrics(ctx, &mx); err == nil { 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) { @@ -178,7 +180,7 @@ func (c *ClusterInfo) fireNoMetaChanged(data ClusterMeta) { // Helpers... -func checkLastestRev() (string, error) { +func fetchLastestRev() (string, error) { log.Debug().Msgf("Fetching latest k9s rev...") ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() diff --git a/internal/model/cluster_info_test.go b/internal/model/cluster_info_test.go index b9579c3d..84155a65 100644 --- a/internal/model/cluster_info_test.go +++ b/internal/model/cluster_info_test.go @@ -6,41 +6,12 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" - "vbom.ml/util/sortorder" ) func init() { 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) { uu := map[string]struct { o, n model.ClusterMeta diff --git a/internal/model/semver.go b/internal/model/semver.go new file mode 100644 index 00000000..20bbc175 --- /dev/null +++ b/internal/model/semver.go @@ -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 +} diff --git a/internal/model/semver_test.go b/internal/model/semver_test.go new file mode 100644 index 00000000..6ab3fd5b --- /dev/null +++ b/internal/model/semver_test.go @@ -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)) + }) + } +} diff --git a/internal/render/node.go b/internal/render/node.go index af59dbb5..fdb70af1 100644 --- a/internal/render/node.go +++ b/internal/render/node.go @@ -89,7 +89,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { no.Status.NodeInfo.KernelVersion, iIP, eIP, - strconv.Itoa(len(oo.Pods)), + strconv.Itoa(oo.PodCount), toMc(c.cpu), toMi(c.mem), strconv.Itoa(p.rCPU()), @@ -134,9 +134,9 @@ func (Node) diagnose(ss []string) error { // NodeWithMetrics represents a node with its associated metrics. type NodeWithMetrics struct { - Raw *unstructured.Unstructured - MX *mv1beta1.NodeMetrics - Pods []*v1.Pod + Raw *unstructured.Unstructured + MX *mv1beta1.NodeMetrics + PodCount int } // GetObjectKind returns a schema object. diff --git a/internal/view/app.go b/internal/view/app.go index 3a8ded39..cbcd4262 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -73,7 +73,7 @@ func (a *App) ConOK() bool { // Init initializes the application. 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) 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.clusterModel = model.NewClusterInfo(a.factory, version) + a.clusterModel = model.NewClusterInfo(a.factory, a.version) a.clusterModel.AddListener(a.clusterInfo()) a.clusterModel.AddListener(a.statusIndicator()) a.clusterModel.Refresh() @@ -115,13 +115,13 @@ func (a *App) Init(version string, rate int) error { } a.CmdBuff().SetSuggestionFn(a.suggestCommand()) - a.layout(ctx, version) + a.layout(ctx) a.initSignals() return nil } -func (a *App) layout(ctx context.Context, version string) { +func (a *App) layout(ctx context.Context) { flash := ui.NewFlash(a.App) 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) 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.toggleCrumbs(!a.Config.K9s.GetCrumbsless()) } func (a *App) initSignals() {