k9s/internal/model/cluster_info.go

243 lines
5.3 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package model
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"sync"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/slogs"
"k8s.io/apimachinery/pkg/util/cache"
)
const (
k9sGitURL = "https://api.github.com/repos/derailed/k9s/releases/latest"
cacheSize = 10
cacheExpiry = 1 * time.Hour
k9sLatestRevKey = "k9sRev"
)
// ClusterInfoListener registers a listener for model changes.
type ClusterInfoListener interface {
// ClusterInfoChanged notifies the cluster meta was changed.
ClusterInfoChanged(prev, curr ClusterMeta)
// ClusterInfoUpdated notifies the cluster meta was updated.
ClusterInfoUpdated(ClusterMeta)
}
// ClusterMeta represents cluster meta data.
type ClusterMeta struct {
Context, Cluster string
User string
K9sVer, K9sLatest string
K8sVer string
Cpu, Mem, Ephemeral int
}
// NewClusterMeta returns a new instance.
func NewClusterMeta() ClusterMeta {
return ClusterMeta{
Context: client.NA,
Cluster: client.NA,
User: client.NA,
K9sVer: client.NA,
K8sVer: client.NA,
Cpu: 0,
Mem: 0,
Ephemeral: 0,
}
}
// Deltas diffs cluster meta return true if different, false otherwise.
func (c ClusterMeta) Deltas(n ClusterMeta) bool {
if c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral {
return true
}
return c.Context != n.Context ||
c.Cluster != n.Cluster ||
c.User != n.User ||
c.K8sVer != n.K8sVer ||
c.K9sVer != n.K9sVer ||
c.K9sLatest != n.K9sLatest
}
// ClusterInfo models cluster metadata.
type ClusterInfo struct {
cluster *Cluster
factory dao.Factory
data ClusterMeta
version string
cfg *config.K9s
listeners []ClusterInfoListener
cache *cache.LRUExpireCache
mx sync.RWMutex
}
// NewClusterInfo returns a new instance.
func NewClusterInfo(f dao.Factory, v string, cfg *config.K9s) *ClusterInfo {
c := ClusterInfo{
factory: f,
cluster: NewCluster(f),
data: NewClusterMeta(),
version: v,
cfg: cfg,
cache: cache.NewLRUExpireCache(cacheSize),
}
return &c
}
func (c *ClusterInfo) fetchK9sLatestRev() string {
rev, ok := c.cache.Get(k9sLatestRevKey)
if ok {
return rev.(string)
}
latestRev, err := fetchLatestRev()
if err != nil {
slog.Warn("k9s latest rev fetch failed", slogs.Error, err)
} else {
c.cache.Add(k9sLatestRevKey, latestRev, cacheExpiry)
}
return latestRev
}
// Reset resets context and reload.
func (c *ClusterInfo) Reset(f dao.Factory) {
if f == nil {
return
}
c.mx.Lock()
{
c.cluster, c.data = NewCluster(f), NewClusterMeta()
}
c.mx.Unlock()
c.Refresh()
}
// Refresh fetches the latest cluster meta.
func (c *ClusterInfo) Refresh() {
data := NewClusterMeta()
if c.factory.Client().ConnectionOK() {
data.Context = c.cluster.ContextName()
data.Cluster = c.cluster.ClusterName()
data.User = c.cluster.UserName()
data.K8sVer = c.cluster.Version()
ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())
defer cancel()
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
}
}
data.K9sVer = c.version
v1 := NewSemVer(data.K9sVer)
var latestRev string
if !c.cfg.SkipLatestRevCheck {
latestRev = c.fetchK9sLatestRev()
}
v2 := NewSemVer(latestRev)
data.K9sVer, data.K9sLatest = v1.String(), v2.String()
if v1.IsCurrent(v2) {
data.K9sLatest = ""
}
if c.data.Deltas(data) {
c.fireMetaChanged(c.data, data)
} else {
c.fireNoMetaChanged(data)
}
c.mx.Lock()
{
c.data = data
}
c.mx.Unlock()
}
// AddListener adds a new model listener.
func (c *ClusterInfo) AddListener(l ClusterInfoListener) {
c.listeners = append(c.listeners, l)
}
// RemoveListener delete a listener from the list.
func (c *ClusterInfo) RemoveListener(l ClusterInfoListener) {
victim := -1
for i, lis := range c.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...)
}
}
func (c *ClusterInfo) fireMetaChanged(prev, cur ClusterMeta) {
for _, l := range c.listeners {
l.ClusterInfoChanged(prev, cur)
}
}
func (c *ClusterInfo) fireNoMetaChanged(data ClusterMeta) {
for _, l := range c.listeners {
l.ClusterInfoUpdated(data)
}
}
// Helpers...
func fetchLatestRev() (string, error) {
slog.Debug("Fetching latest k9s rev...")
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, k9sGitURL, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
if resp.Body != nil {
_ = resp.Body.Close()
}
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
m := make(map[string]interface{}, 20)
if err := json.Unmarshal(b, &m); err != nil {
return "", err
}
if v, ok := m["name"]; ok {
slog.Debug("K9s latest rev", slogs.Revision, v.(string))
return v.(string), nil
}
return "", errors.New("no version found")
}